# 들어가며
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 3가지 계층간 구조와 url호출을 어떤방식으로 주고받는지, 또 도메인 모델 속성이 외부에 노출되면 보안 문제가 발생할 수 있기에 dto를 사용한 부분등 참 많은것을 알고가는 주차였다. 특히 3주차는 프로젝트를 먼저 만들어보는것이 중심이였기에 만들면서 궁금증이 많았는데 그중 RequiredConstructor어노테이션을 캠프내에 잘하는분께 여쭤보기도 하고 구글링도 하였으나 뭔가 자세하게 알고 가지 못한것같다는 생각이 강했다. 그런데 금요일에 4주차 발제를 받고 DI -> IoC -> Bean 순서로 지식을 하나씩 쌓아나갔는데 이 모든과정을 알고나니 @RequiredConstructor의 사용이유를 시작으로 프로젝트가 내부적으로 어떻게 돌아가는지, 스프링이 왜 DI와 IoC가 핵심인지 명확하게 짚고 넘어갈수 있었다.
지금 작성하면서도 "DI는 컨트롤러,서비스,레퍼지토리간 결합을 강하게한뒤, 나중에 변경사항 요청이오면 모든 클래스를 바꿔줘야 하기때문에 생성자에서 객체를 생성하는것이 아닌 , 매개변수로 객체를 주입받는 형식으로 느슨한 결합 형태로 바꿔주기 위해 사용한다. 또한 일반적인 경우엔 해당클래스내에서 다른 객체를 생성해주어서 사용하였지만, 위와 같은 상황을 우려하여 DI를 적용한 형태로 바꿔주면 두 경우를 비교했을때 역전이 일어나게되고 이것을 IoC라고 부른다. 그럼 DI를 적용한 형태로 바꿔주었을때 객체를 생성하고, 관리해줄곳이 필요한데 관리해주는곳은 바로 IoC컨테이너이며 IoC컨테이너는 Bean으로 설정된 클래스나, 직접 생성하여 Bean처리를 해준 객체를 관리한다. 따라서 @Service,@Controller등 어노테이션이 붙은 클래스는 해당 클래스를 빈으로 관리되게 만드는 부모 어노테이션인 @Component를 호출하게 되고, 이 클래스를 객체로 만든 후 IoC컨테이너에서 관리된다. 결과적으로 Lombok기능중 하나인 @RequiredConstructor는 스프링형태의 클래스에서 DI설정을 위해( = 느슨한 결합을 위해 )final로 멤버변수를 설정하고, DI설정을 위해 생성자의 매개변수로 객체를 주입받게 되는 형태를 띄는데 이때 생성자를 생략해준다." 이렇게 줄줄 스토리가 나오는데, 그 내부적인 구조를 알고나니 정말 시원한(?)느낌이 드는것같다.
사실 전에는 "스프링부트를 활용한 웹 프로젝트"를 진행하면서 무작정 책을 따라하며 이해가안되는 부분을 눈으로 계속 보면서 "전부 알고갔다"라는 확신이 없는채로 지나갔지만 이렇게 하나씩 기본적인 것을 짚고가게되니 방대한 스프링에 한발짝 다가선것같은 느낌이드는것같다. 1주차에선 회고록을 쓸때 무슨의미가 있을까 싶었으나 이제슬슬 내가 정리한 내용을 하나씩보면서 오히려 생각이 잘 정리되는듯한 느낌을 받는것같다.
'항해 부트캠프 > 항해99' 카테고리의 다른 글
항해99 ) 4주차 회고록 (0) | 2022.07.17 |
---|---|
항해99 ) 2주차 회고록 (0) | 2022.07.03 |
항해99 ) 첫 1주차(팀 단위 미니프로젝트) 프로젝트 (0) | 2022.06.22 |