# 들어가며
3주차는 주특기 입문주차로, Springboot를 사용하여 큰 프로젝트내에서 Controller,Service,Repository에 맞는 파일을 넣어보고, URL이 어떤방식으로 호출되는지 알아보는 주차였다.
강의로 진행한 프로젝트는 크게 CRUD를 사용하여 웹에 메모를 남길수있는 프로젝트, 네이버 쇼핑API를 이용하여 상품을 검색한뒤 클라이언트가 상품의 최저가를 설정하고 상품이 설정한 최저가보다 가격이 낮으면 최저가박스가 뜨게하는 프로젝트 두가지를 진행하였다.
# 개인 과제
개인 과제 요구사항은 5개로
1.전체 게시글 목록 조회 API ( 제목, 작성자명, 작성 날짜를 조회, 작성 날짜 기준으로 내림차순 정렬)
2. 게시글 작성 API ( 제목, 작성자명, 비밀번호, 작성 내용 입력)
3. 게시글 조회 API ( 제목, 작성자명, 작성 날짜, 작성 내용 조회)
4. 게시글 수정 API ( API 호출 시 입력된 비밀번호를 비교하여 동일할 때만 글이 수정 )
5. 게시글 삭제 API ( API 호출 시 입력된 비밀번호를 비교하여 동일할 때만 글이 삭제 )
HTML은 따로 구성하지 않아도되며, controller와 service정도까지 구성하는 프로젝트였다.
처음에 HTML이 따로 존재하지않으면 결과물을 ARC같은 툴을 이용하여 URL을 직접 입력 해야했기 때문에 HTML을 백지상태에서 따로 만들어볼까 생각하였으나 막막하여 강의에서 나온 메모장 프로젝트를 뜯어고쳐서 만들기로 하였다.
URL자체는 메모장 프로젝트와 상당히 유사하기 때문에 백지상태에서 기억을 더듬어 메모장 프로젝트Controller 코드를 전부 지운뒤 빠르게 짤수있었다. Entity는 기존 메모장 프로젝트에서 비밀번호, 작성자명,제목 정도를 추가하고 Dto도 마찬가지로 추가해주면 되었기에 빠르게 완성하였다. 시간을 많이 할애한 부분은 다른 프로젝트를 가져와 고치는 형식이였기에 html에서 jQuery로 값을 가져온뒤, AJAX로 호출하는 부분에서 많은 시간을 할애하였다. 가장 시간이 많이 걸린 부분은 Password가 동일할 때 글이 수정,삭제되는 부분이였는데 어떻게 할지 한참 고민하다가 Ajax안에 Ajax를 사용하는 방식으로 하였다.
(과제 요구사항은 URL을 만드는것이 주 목적이였기에 HTML이 상당히 저퀄이다)
# 처음 만든 코드
// 메모 수정
function submitEdit(id) {
let inputpassword = $(`#${id}-textarea2`).val();
// 비밀번호가 같은지 확인
$.ajax({
type: "GET",
url: `/api/memos/${id}`,
success: function(response){
let password = $(`#${id}-password`).text().trim();
if(inputpassword == password){
// 작성 대상 메모의 username과 contents확인
let writer = $(`#${id}-writer`).text().trim(); // 읽기모드(텍스트가 박혀있는)는 .val()이 아닌 .text로 가져온다.
let contents = $(`#${id}-textarea`).val().trim();
let password = $(`#${id}-password`).text().trim();
let title = $(`#${id}-title`).text().trim();
let data = {'password': password, 'title':title, 'writer': writer, 'contents': contents };
// PUT /api/memos/{id} 에 data를 전달합니다.
$.ajax({
type: "PUT",
url: `/api/memos/${id}`,
contentType: "application/json",
data: JSON.stringify(data),
success: function (response) {
alert('메시지 변경에 성공하였습니다.');
window.location.reload();
}
});
}
else{ return alert("비번이 다릅니다."); }
}
})
}
먼저 메모를 수정,삭제할때 입력된 비밀번호를 jQuery로 inputpassword변수에 가져온 뒤, 첫번째 ajax에서 GET방식으로 api를 호출하여 비밀번호를 바꿀 해당 메모id의 비밀번호를 변수 password에 저장하고, 만약 두 변수가 동일하다면 다시 ajax를 호출하여 PUT방식으로 api를 호출한뒤, 업데이트 되게끔 만들었다. 만들고나니 뭔가 ajax안에 ajax를 호출하는 부분도 그렇고, 이걸 여기서 만드는게맞나? 라는 생각이 들어서 제출전 매니저님한테 피드백을 받고 새롭게 코드를 짜게되었다.
# 피드백 받은 후 코드 ( html )
function submitEdit(id) {
let password = $(`#${id}-textarea2`).val(); // 입력받은 비번
let writer = $(`#${id}-writer`).text().trim(); // 읽기모드(텍스트가 박혀있는)는 .val()이 아닌 .text로 가져온다.
let contents = $(`#${id}-textarea`).val();
let title = $(`#${id}-title`).text().trim();
let data = {'password': password, 'title': title, 'writer': writer, 'contents': contents};
//PUT /api/memos/{id} 에 data를 전달
$.ajax({
type: "PUT",
url: `/api/memos/${id}`,
contentType: "application/json",
data: JSON.stringify(data),
success: function (response) {
console.log(response);
window.location.reload();
}
});
}
새롭게 바뀐 코드는 비밀번호가 동일할시 수정을 할때, ajax는 PUT요청만 보내고 해당 로직을 Service계층에 구현하는것이다. 확실히 코드 구성을 보자마자 매니저님은 "그냥 서비스에 구현하시면 되겠는데요?~" 라고 말씀하셔서 아.. 역시 현업에 계신분은 다르구나 느꼈다. Service코드는 아래와 같다.
# Service
@Transactional
public Long update(Long id,MemoRequestDto memoRequestDto){
Memo memo = memoRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("아이디가 없습니다.")
);
if(memo.getPassword().equals(memoRequestDto.getPassword())) {
memo.update(memoRequestDto);
}
return id;
}
기존 update() 에서 if문이 추가된 형식으로 바뀌었다. PUT으로 요청할때 Controller에서 data가 모두 dto형식으로 전달되고, id 또한 넘겨받기에 id가 동일한 객체를 memo변수에 넣어주면 memo의 패스워드와 넘겨받은 dto의 패스워드를 각각 Getter로 비교하여 업데이트 시켜주면 간단하게 만들수있었다. 여기서 멍청하게도 각 패스워드를 Get으로 비교하는 부분에서 .equals이 아닌 ==를 사용하였다가 다른 곳에 문제가 있는지알고 30분을 할애하였다.
# 궁금한점
프로젝트를 진행하면서 @RequiredArgsConstructor와 @NoArgsConstructor등 어노테이션을 종종 사용하였는데 해당 내용을 작성하면서 강사님이 대충 Lombok의 기능으로 생성자를 자동으로 만들어준다 정도로만 이해하고 넘어갔는데 특히 @RequiredArgsConstructor는 보다보다 도저히 어떤것인지 궁금하여서 직접 찾아보곤했는데 나중에 안 사실이지만 4주차 1강에 해당내용이 있었다.. 따라서 해당 어노테이션과 관련되어 새롭게 알게된 DI,IoC컨테이너,Bean 등등 아래에서 자세히 다뤄볼것이다.
# DI ( Dependency Injection , 의존성 주입 )
먼저, 우리가 늘 사용하는 Service와 Repository를 생각해보자.
public class Service1 {
private final Repository1 repository1;
public Service1() {
this.repository1 = new Repository1();
}
}
위 코드는 Service1생성자 안에서 Repository1 객체를 생성하여 사용하고있다.
public class Repository1 {
public Repository1(String id, String pw) {
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", id, pw);
}
}
Repository1 클래스에선 Repository1 객체 생성시 DB 접속 id, pw를 받아서 DB접속 시 사용하도록 만들었다고 가정한다.
만약 위처럼 사용하게 된다면 아래와 같은 상황이 발생한다.
# 강한 결합
만약 Controller가 5개가 있고 모두 Service1을 생성하여 사용 중이라고 가정하면 Repository1 생성자 변경에 의해 모든 Controller와 모든 Service 의 코드 변경이 필요하다.
그렇다면 강한결합을 해결하기 위해선 어떻게해야할까?
👉 "강한결합"의 해결방법
1. 각 객체에 대한 객체 생성은 딱 1번만
2. 생성된 객체를 모든 곳에서 재사용
#강한 결합 해결
public class Repository1 { ... }
// 객체 생성
Repository1 repository1 = new Repository1();
Repository1클래스를 선언하고 어디선가 Repository1객체를 생성해준다.(나중에 설명)
Class Service1 {
private final Repository1 repository1;
// repository1 객체 사용
public Service1(Repository1 repository1) { //매개변수에 Repository1가 추가되었음(DI의 핵심)
this.repository1 = new Repository1(); // 잘못된 예
this.repository1 = repository1; // 옳은 예
}
}
// 객체 생성
Service1 service1 = new Service1(repository1);
다음으로 Service1클래스내에서 멤버변수 repository1에 저장한뒤, 생성자의 매개변수로 Repository1 객체를 받아준다.(이것이 DI라고 볼수있음) 또, service1객체를 어디선가 생성해준다.
위 그림에선 service1에서 repository1 객체를 생성하는것은 절대 안된다고 표현하고있다. repository1객체는 Repository1클래스를 가르키고있고, 단지 Service객체를 생성할때 매개변수에 repository1를 주입하고있다.
public class Repository1 {
public Repository1(String id, String pw) {
// DB 연결
Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", id, pw);
}
}
// 객체 생성
String id = "sa";
String pw = "";
Repository1 repository1 = new Repository1(id, pw);
따로 객체 생성을 해주어, 객체 생성을할때 id와 pw를 설정하게 해준다.
만약 위와같이 코드변경이 일어난다면 Repository1 생성자의 변경은 이제 누구에게도 영향을 끼치지 않게되었고, Service1 생성자가 변경되면 모든 Controller의 변경이 필요없어지게 되었다. 결론적으로 느슨한 결합이 되었다.
여기서 Service1이 repository1을 가져다가 사용하지만 Repository1이 어떤 아이디와 패스워드를 가지고 만들어졌는지는 알수가 없다. 마찬가지로 Controller1,2,3,4,5이 service1을 쓸때도 service1이 어떤방식으로 만들어졌는지 알수가 없다.
결론적으로 위와 같이 제어의 역전( IoC:Inversion of Control )이 일어나게된다.
👉"제어의 역전(Inversion of Control)"
: 프로그램의 제어 흐름이 뒤바뀜
일반적으로 사용자는 자신이 필요한 객체를 생성해서 사용하지만 Ioc는 용도에 맞게 필요한 객체를 그냥 가져다 사용(DI)하며, 사용할 객체가 어떻게 만들어졌는지는 알수가 없다. 가위의 용도별 사용을 예로들어보면 음식을 자를때 필요한 가위는 부엌가위(생성된 객체 kitchenScissors), 무늬를 낼때 필요한 가위는 핑킹가위(생성된 객체 pinkingShears),정원의 나무를 다듬을 때 필요한 가위는 전지가위(pruningShears)등등 다양한데 이렇게 생성된 객체를 그저 가져다가 사용하는것 뿐이다.
# 그래서 객체 생성 위치는?
그렇다면 이제 위에서 설명한 Repository1객체와 Serivce1객체를 new로 생성해줘야 하는데 과연 어디서 해야할까? 이럴때 스프링 프레임워크가 필요한 객체를 생성하여 관리하는 역할을 대신해준다.
👉빈(Bean) : 스프링이 관리하는 객체
👉스프링 IoC 컨테이너 : '빈'을 모아둔 통
# 스프링 '빈'등록 방법
//클래스위에 @Component로 설정해준다.
@Component
public class ProductService { ... }
위와 같이 클래스를 빈으로 설정해주면 스프링 IoC컨테이너에서 관리되게 된다.
이때, @Componenet 적용조건이 있는데
// @Component 적용조건
@Configuration
@ComponentScan(basePackages = "com.sparta.springcore")
class BeanConfig { ... }
@ComponentScan이 필요하다. 이것은 basePackages에 적힌 패키지 내에있는 Component를 찾는다는 의미이다. 따라서 ComponentScan 패키지 내에있는 클래스만 빈으로 등록된다.
@Configuration설정을 해주면 스프링이 기동될때 해당클래스를 먼저 찾아 읽는다.
여기서 @SpringBootApplication이 있다면 해당 어노테이션의 default값으로 해당 어노테이션이 들어있는 패키지가 자동으로 ComponentScan으로 등록된다. 위의 @SpringBootApplication을 Ctrl+클릭해보면
자동으로 @ComponentScan이 등록되어 있는것을 볼수있다.
# 직접 객체를 생성하여 빈으로 등록 요청
예를들어 아래와같이 Repository의 객체에 @Component적용을 시켜준다.
생성자에서 각 매개변수가 오류가 뜨는것을 볼수있다. 이는 클래스를 IoC컨테이너에서 관리해야하는데 스프링이 해당 변수들이 어디서 어떻게 들어올지 모르기 때문에 오류처리가 나는것이다.
따라서 위처럼 @Configuration을 클래스위에 먼저 설정해주고(@Configuration가 설정된 클래스는 스프링이 처음 기동될때 해당 클래스를 먼저 읽는다.) 그리고, 생성자에 @Bean설정을 해준다면 return값이 Bean으로 들어가며 스프링 IoC컨테이너에서 관리되게 된다.
여기서 스프링 '빈'이름은 @Bean이 설정된 함수명이며 첫글자를 소문자로 취급하여 넣어준다.(위에선 productRepository)
# 스프링 '빈'사용 방법
👉" 스프링'빈' 사용 방법 "
: '빈'을 사용한 멤버변수나,함수위에 @Autowired를 설정해준다.
@Component
public class ProductService {
@Autowired
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// ...
}
@Autowired적용조건은 스프링 IoC컨테이너에 의해 관리되는 클래스에서만 가능(@Component가 적용된 클래스)하다.
또, @Autowired 생략이 가능한데 Spring4.3버전 부터 생성자가 1개 일때만 생략이 가능하다.
public class A {
@Autowired // 생략 불가
public A(B b) { ... }
@Autowired // 생략 불가
public A(B b, C c) { ... }
}
위는 생성자가 2개이므로 생략이 불가능하다.
# @RequiredArgsConstructor
그럼 내가 이번 주차에서 느꼈던 대망의 이 모든 궁금증의 시발점인 @RequiredArgsConstructor는 무엇인가?
@Component
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// ...
}
위에서 productRepository가 final멤버변수로 선언되고 있고, @Autowired로 Bean적용을 받고 DI가 적용된 생성자를
@RequiredArgsConstructor
@Component
public class ProductService {
private final ProductRepository productRepository;
// ...
}
생략이 가능하게 해준다. 따라서 자동으로 'DI가 적용된 생성자 주입'을 해준다고 볼수있다.
👉"@RequiredArgsConstructor"
final이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 롬복 어노테이션
# 마지막으로 스프링 3계층 Annotation
👉"스프링 3계층 Annotation"
스프링은 3계층 @Service , @Controller , @Repository 어노테이션을 제공하고있는데 이 모든 어노테이션들은 default값으로 @Component가 설정되어있다. 따라서 해당 어노테이션이 붙은 클래스들은 모두 IoC컨테이너에서 관리되고 있다.
그러나,여기서 JPA를 사용한다면 아래와같이 @Repository는 인터페이스로 따로 @Repository어노테이션을 쓰지않는다.
이는 스프링 DAta JPA에 의해 자동으로 @Repository가 추가되기 때문에 생략이 가능하다.
# 느낀점
3주차는 Controller - Service - Repository 구조를 직접 만들어보면서, 각 계층이 어떤 역할을 하고 어떻게 서로 데이터를 주고받는지를 몸으로 느껴볼 수 있었던 시간이었다. 프로젝트를 만들면서 URL 호출 방식이나, 도메인 모델의 속성을 그대로 외부에 노출하면 안 되기 때문에 DTO를 활용해야 한다는 점도 새삼 중요하게 다가왔다.
이번 주는 전체적으로 실습 위주라 그런지, 궁금한 게 참 많았던 한 주였다. 특히 @RequiredArgsConstructor라는 어노테이션이 처음에는 익숙하지도 않고 정확히 어떤 역할을 하는지도 잘 몰라서, 캠프에서 잘하는 분께 물어보기도 하고 구글링도 해봤는데 뭔가 겉핥기만 한 느낌이 강하게 남았다.
그러다 금요일에 4주차 발제를 받고 나서 DI → IoC → Bean 순서로 하나씩 정리해보니, 퍼즐 조각들이 맞춰지는 기분이 들었다.
"아, 그래서 @RequiredArgsConstructor를 쓰는 거구나" 하는 이해가 비로소 됐다.DI는 컨트롤러, 서비스, 레포지토리 간의 결합을 느슨하게 만들어주는 개념인데, 예전에는 클래스 안에서 직접 객체를 생성해서 쓰는 방식이 보통이었다. 그런데 그럴 경우엔 변경이 생기면 전부 다 고쳐야 하니까 유지보수가 너무 어렵다. 그래서 객체를 생성하는 게 아니라 매개변수로 받아서 쓰는 식으로 바꾸는 게 DI고, 이게 결국 흐름을 역전시키는 거라서 IoC라고 부르는 거였다.
이렇게 DI를 적용하게 되면 객체를 대신 만들어서 관리해주는 존재가 필요한데, 그게 바로 IoC 컨테이너고, 이 컨테이너는 @Service, @Controller 같은 어노테이션이 붙은 클래스들을 Bean으로 등록해서 관리해준다.결과적으로 @RequiredArgsConstructor는 이런 DI 구조를 편하게 쓰기 위해서, final로 선언된 멤버 변수들을 매개변수로 받는 생성자를 자동으로 만들어주는 역할을 한다. DI를 위해 꼭 필요한 생성자를 손으로 일일이 안 써도 되게끔 도와주는 셈이다. 이제는 이 어노테이션 하나가 단순히 코드를 줄이기 위한 도구가 아니라, 스프링 구조의 흐름 속에서 어떤 의미를 가지는지 좀 더 명확히 이해하게 된 것 같다.
사실 예전에는 스프링부트 책을 따라 하면서도 왜 그렇게 해야 하는지 이해가 안 된 채로, 그냥 눈으로만 보고 넘어갔던 부분이 많았다. 뭔가 “전부 알고 간다”는 느낌 없이 지나친 적이 많았는데, 이번에는 개념 하나하나를 정리하면서 “이래서 그렇구나” 하고 확실히 납득이 되니 그동안의 모호함이 조금씩 해소되는 느낌이 들었다.
'항해 부트캠프 > 항해99' 카테고리의 다른 글
항해99 ) 4주차 회고록 (0) | 2022.07.17 |
---|---|
항해99 ) 2주차 회고록 (0) | 2022.07.03 |
항해99 ) 첫 1주차(팀 단위 미니프로젝트) 프로젝트 (0) | 2022.06.22 |