다음은 글은 infrean의 "자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]" 강의의 학습 목적으로 작성된 것입니다. 강의의 디테일한 내용이나, 코드 등은 빠져있을 수 있습니다.
1. 좋은 코드(Clean Code)는 왜 중요한가?
코드는 요구사항을 표현하는 언어이다.
개발자는 요구사항을 구현하기 위해 기존 코드를 읽고 작성한다.
코드를 읽는 것은 필수적이고 피할 수 없다.
특히, 협업에서는 "남의 코드를 읽고 해석"하는 일이 대부분....
안 좋은 코드가 쌓이면, 시간이 지날수록 생산성이 낮아진다.
... 더 자세한 내용은 로버트 C. 마틴의 책 <클린 코드>를 읽어보자...
그 중에서 가장 소프트웨어 공학적으로 강조하는 것은 "단일 책임 원칙(SRP)"
각 함수나 클래스는 하나의 목적이나 책임을 가져야 한다는 뜻이다.
1. 함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.
2. 클래스는 작아야 하며 하나의 책임만을 가져야 한다.
현재 Controller는 3가지의 일을 하고있다....
1. API의 진입 지점으로써 HTTP Body를 객체로 변환하고 있다.
2. 현재 유저가 있는지, 없는지 등을 확인하고 예외 처리를 해준다.
3. SQL을 사용해 실제 DB와의 통신을 담당한다.
2. 역할별로 3단 분리하기 ; MVC 패턴
1. API의 진입 지점으로써 HTTP Body를 객체로 변환하고 있다. => Controller
2. 현재 유저가 있는지, 없는지 등을 확인하고 예외 처리를 해준다. => Service
3. SQL을 사용해 실제 DB와의 통신을 담당한다. => Repository
Controller : HTTP 요청을 받아 해당 요청에 대한 응답을 생성하는 역할을 담당. HTTP 엔드포인트와 매핑되어 있다.
Service : 비즈니스 로직을 처리. 컨트롤러로부터 받은 요청에 대한 실질적인 비즈니스 로직을 구현.
Repository : 데이터베이스와 직접적으로 통신하며 데이터의 저장, 검색, 갱신, 삭제 등을 처리.
DTO : 데이터를 전송하는 데 사용되는 객체. 레이어 간의 데이터 교환에 사용.
// Entity : 데이터베이스에서 관리되는 실제 데이터 모델. 주로 JPA와 같은 ORM을 사용하여 DB와 상호작용.
해당 패턴을 MVC 패턴이라고 한다.
수정된 코드 : (PUT API 만 공개)
// Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// POST API
// GET API
// PUT API
public boolean isUserNotExist(long id) {
String readSql = "SELECT * FROM user WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0 , id) . isEmpty();
}
public void updateUserName(String name, long id) {
String sql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, name, id);
}
// DELETE APT
}
// Serivce
public class UserService {
private final UserRepository userRepository;
public UserService(JdbcTemplate jdbcTemplate) {
userRepository = new UserRepository(jdbcTemplate);
}
// POST API
// GET API
// PUT API
public void updateUser(UserUpdateRequest request) {
if (userRepository.isUserNotExist(request.getId())) {
throw new IllegalArgumentException();
}
userRepository.updateUserName(request.getName(), request.getId());
}
// DELETE API
}
// Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// POST API
// GET API
// PUT API
public void updateUserName(String name, long id) {
String sql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, name, id);
}
public boolean isUserNotExist(long id) {
String readSql = "SELECT * FROM user WHERE id = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0 , id) . isEmpty();
}
// DELETE APT
}
🤔 "new UserController(jdbcTemplete)" 이란 코드는 작성한 적이 없는데, 어떻게 JdbcTemplate이 Controller에 전달됐을까?
🤔 계속 건내주는 과정을 거치는데, Repository에서 바로 JdbcTemplate을 가져올 수는 없을까?
3. UserController와 스프링 컨테이너
3-1. UserController에서의 궁금증
[1] static이 아닌 코드를 사용하려면 인스턴스화가 필요하다.
.. 우리는 생성자까지만 작성하고, 인스턴스화 해준 적이 없다.. < new UserController(jdbcTemplete) X >
도대체 누가 UserController를 인스턴스화 하고 있는것인가?!
[2] UserController는 JdbcTemplate이 필요하다.
= UserController는 JdbcTemplate에 의존하고 있다.
= UserController는 JdbcTemplate이 없으면 동작하지 않는다!
그런데 우리는 JdbcTemplate이란 클래스를 설정해준 적이 없다.
UserController는 어떻게 JdbcTemplate을 가져올 수 있었을까?
이러한 일들을 " @RestController " Annotation이 해준다!!
3-2. 스프링 빈
@RestController 어노테이션이 적용된 클래스는 Spring MVC에서 해당 클래스를 컨트롤러로 인식하고, 스프링 빈으로 등록시킨다.
이로 인해, 다른 컴포넌트에서 이를 주입해서 사용할 수 있다.
서버가 시작되면, 스프링 서버 내부에 거대한 스프링 컨테이너를 만들게 된다.
스프링 빈(Spring Bean)은 스프링 프레임워크가 관리하는 객체를 말한다.
이는 스프링 컨테이너에 등록되어, 스프링의 라이프사이클에 따라 관리되고 주입되는 객체를 의미한다.
이렇게 스프링 컨테이너에 등록될 때, 다양한 정보도 함께 들어가고, 인스턴스화도 이루어진다.
스프링은 빈을 관리하고 DI (의존성 주입)을 통해 객체 간의 관계를 관리한다!
UserController를 인스턴스화 하려면, 파라미터로 JdbcTemplate 또한 필요했다!!
-> 사실 이 JdbcTemplate도 스프링 빈으로 등로되어 있다.
그렇다면, JdbcTemplate은 누가 스프링 빈으로 등록시켜 주었는가?
(최소한, UserController 생성자는 우리가 코드라도 짜놨다.. 그걸 어노테이션이 대신 해줬다는건 논리적으로 이해가 된다.
그런데 JdbcTemplate는 도대체 누가?)
비밀은 build.gradle에 설정해 놓은 dependencies(의존성)에 있다.
🫛🫛🫛이 왼쪽의 완두콩이 바로 "스프링의 빈"을 뜻한다!
해당 버튼을 누르면, 자동으로 의존성이 주입된 곳을 가르켜 준다.
따라가 보면 JdbcTemplate에 대한 코드가 있다.
springboot - autoconfigure에 있는 클래스 JdbcTemplateConfiguration이 있는 것을 알 수 있다.
이 클래스는 바로, build.gradle에 설정해 놓은 dependencies(의존성) Starter에 포함되어 있고,
이에 의해서 외부 의존성이 설정된 것이다.
* 실제로 'org.springframework.boot:: spring-boot-starter-data-jpa'를 주석처리하고 실행해보면
다음과 같이 에러가 나는 것을 알 수 있다. (왼쪽에 콩 모양🫛도 사라졌다 ㅠㅠㅠㅠ)
3-3. 서버를 실행시키면 일어나는 일
- 스프링 컨테이너(클래스 저장소)가 시작된다.
- 기본적으로 많은 스프링 빈들이 등록된다. ex) JdbcTemplate, DataSource, Environmet 등
- 우리가 설정해준 스프링 빈들이 등록된다. ex) UserController 등
- 이때 필요한 의존성이 자동으로 이뤄진다. ex) Usercontroller는 JdbcTemplate에 의존하고 있다.
* JdbcTemplate을 가져오려면
UserService, UserRepository가 스프링 빈이어야 하는데
UserSerivce, UserRepository는 스프링 빈이 아니다!
그래서 일일이 의존성을 생성자에 파라미터로 주입해줬다...
=> 둘 다 등록시켜주면 되지 않아??
3-4. UserService, UserRepository를 스프링 빈으로 등록하기
@Service 와 @Repository 어노테이션을 사용하여 스프링 빈으로 등록해준다.
// 생성자 주입 (Constructor Injection)
@Service
public class MyService {
private final MyRepository myRepository;
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
// ...
}
이렇게 된다면,
Repository만이 JdbcTemplate을 불러오고,
Service와 Controller 코드에는 JdbcTemplate을 모두 없앨 수 있다.
3-5. 결론 ; 서버를 실행시키면 일어나는 일
- 스프링 컨테이너(클래스 저장소)가 시작된다.
- 기본적으로 많은 스프링 빈들이 등록된다. ex) JdbcTemplate, DataSource, Environmet 등
- 우리가 설정해준 스프링 빈들이 등록된다. ex) UserController 등
3-2. UserRepository를 의존하는 UserService가 스프링 빈으로 등록된다.
3-3. UserService를 의존하는 UserController 또한 스프링 빈으로 등록된다. - 이때 필요한 의존성이 자동으로 이뤄진다. ex) UserController는 JdbcTemplate에 의존하고 있다.
🤔 그런데, 그냥 new 연산자를 쓰면 안되나? 스프링 컨테이너를 왜 사용하는 거지?
4. 스프링 컨테이너를 왜 사용할까?
4-1. New 생성자를 쓰는 상황을 가정
예를 들어 data를 메모리에 저장하는 코드와 MySQL에 저장하는 코드가 있다.
이는 데이터 통신 처리를 담당하는 Repository의 역할이다.
우리는 이 역할을 어떤식으로 할 지 (메모리로? MySQL로?) 선택하는 것은
절대적으로 Repository의 코드 수정으로만 수행 가능하도록 해야한다.
(생성자를 사용하면, UserRepository를 의존하고 있는 UserService 또한 건드려줘야한다.
만약에 UserRepository를 의존하는 Service들이 100개이다??..... 밤새야된다.)
public class UserService {
// private final UserMemoryRepository userRepository = new UserMemoryRepository();
private final UserMySqlRepository userRepository = new UserMySqlRepository();
}
4-2. 어떻게 하면 Repository의 Class를 바꾸더라도, Service를 변경하지 않을 수 있을까?
1) Java의 Interface를 이용하자!
Interface UserRepository를 하나 만들고
UserMemoryRepository 나 UserMySqlRepository는 implement UserRepository 해주면,
public class UserService {
// private final UserRepository userRepository = new UserMemoryRepository();
private final UserRepository userRepository = new UserMySqlRepository();
}
new 뒤에 '어떤 구현 타입의 인스턴스'인지만 바꿔주면 된다.
.....그러나.... 어차피 UserService의 일부를 바꾸는 것은 매한가지.. 일이 조금 줄은 것 뿐이다.
4-3. 제어의 역전(IoC)와 의존성 주입(DI)
2) 스프링 컨테이너
스프링 컨테이너가 Service를 대신 인스턴스화 하고, 그 때 알아서 필요한 의존성(Repository)을 결정해준다!
- 이를 제어의 역전 (IoC, Inversion of Control) 이라 한다.
스프링은 IoC 컨테이너로서, 개발자가 객체의 생성과 관리를 제어하는 대신 스프링 컨테이너에게 제어의 권한을 넘긴다.
이로써 애플리케이션의 흐름을 스프링이 제어하게 됩니다.
- 컨테이너가 선택해 BookService에 넣어주는 과정을 의존성 주입(DI, Dependency Injection)라고 한다.
4-4. @Primary를 활용해서 선택
그렇다면 둘 중 어떤 Repository가 주입될까?
(사실 이렇게 두 가지 Repository를 의존하게 되면 에러가 난다)
@Primary 어노테이션을 사용하여 선택하자!
5. 스프링 컨테이너를 다루는 방법
5-1. 빈을 등록하는 방법
@Configuration
- 클래스에 붙이는 어노테이션
- @Bean을 사용할 때 함께 사용해 주어야 한다.
@Bean
- 메소드에 붙이는 어노테이션
- 메소드에서 반환되는 객체를 스프링 빈에 등록한다.
위 두 조합은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.
@Configuration+@Bean은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.
@Service, @Repository
- 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용한다.
@Component
- 주어진 클래스를 '컴포넌트'로 간주한다.
- 이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.
- @Service, @Repository, @Restcontroller에는 @Component가 들어있다.
- 컨트롤러, 서비스, 리포지토리가 모두 아니고
개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.
5-2. 스프링 빈을 주입 받는 방법 (의존성 추가하기)
1. 생성자 주입 (Constructor Injection) <권장>
스프링에서는 주로 생성자 주입을 사용하여 의존성을 주입
빈을 생성하는 생성자에서 필요한 의존성을 매개변수로 받는다.
public class MyService {
private final MyRepository myRepository;
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
// ...
}
2. Setter 주입 (Setter Injection)
빈의 setter 메서드를 통해 의존성을 주입하는 방법
누군가 setter를 사용하면 오작동할 수 있다.
public class MyService {
private MyRepository myRepository;
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
// ...
}
3. 필드 주입 (Field Injection) by @Autrowired
필드에 직접 의존성을 주입하는 방법
테스트하기 어려울 뿐 아니라 의존성 주입을 명시적으로 확인하기 어려워질 수 있기 때문에 권장되지 않는다.
public class MyService {
@Autowired
private MyRepository myRepository;
// ...
}
@Qualifier
- @Primary 어노테이션이 없는 상황에서 빈을 받는 쪽에서 특정 빈을 선택할 수 있게 해준다.
- 받는 쪽과 주는 쪽 둘 다 @Qualifier("특정이름")을 적어주면 서로 연결된다.
// 사용하는 쪽만 @Qualifier를 사용할 경우, 해당 클래스의 이름의 앞부분만 소문자로 바뀌어 스프링 빈에 등록된 것을 사용하면 된다.
@Component
public class MyFirstService implements MyService {
// ...
}
@Component
public class MySecondService implements MyService {
// ...
}
@Component
public class MyClient {
private final MyService myService;
@Autowired
public MyClient(@Qualifier("myFirstService") MyService myService) {
this.myService = myService;
}
// ...
}
- 둘 다 사용하고 있을 경우 @Qualifier가 우선순위가 더 높다.
@Primary vs @Qualifier
사용하는 쪽에 직접 적어준 @Qualifier가 이긴다. (명시적으로 적어준 쪽이 승리)