[TIL] 스프링 입문 - DTO와 계층형 아키텍처
DTO의 역할과 Entity 대신 DTO를 통해 데이터를 주고받는 이유를 정리한 TIL입니다.
For the English version of this post, see here.
오늘 할 일
공부한 내용
DTO
- DTO (Data Transfer Object)
- 데이터 전송 및 이동을 위해 생성되는 객체
- Client에서 보내오는 데이터를 객체로 처리할 때 사용됨
- 서버의 계층간의 이동에 사용된
- DB와의 소통을 담당하는 Java 클래스를 그대로 Client에 반환하는 것이 아니라 DTO로 한번 변환한 후 반환할 때도 사용됨
- Request의 데이터를 처리할 때 사용되는 객체는 RequestDto, Response를 할 때 사용되는 객체는 ResponseDto라는 이름을 붙여 DTO 클래스를 만들 수 있음
- 절대적인 규칙은 아님
Database
- Database
- 데이터의 집합
- DBMS (Database Management System): Database를 관리하고 운영하는 소프트웨어
- RDBMS (Relational DBMS): 관계형 데이터베이스 ex) Oracle, MySQL
- table이라는 최소 단위로 구성, 각 table은 열(column)과 행(row)으로 이루어져있음
SQL
JOIN 이해하기
- 나누어진 테이블을 하나로 합치기 위해 데이터베이스가 제공하는 기능
ON이라는 키워드를 통해 기준이 되는 컬럼을 선택하여 2개의 테이블을 합쳐줌- JOIN 할 때는 적어도 하나의 컬럼을 서로 공유하고 있어야 하기 때문에, 테이블에 외래 키가 설정되어있다면 해당 컬럼을 통해 JOIN을 하면 조건을 충족할 수 있음 다만, JOIN을 하기 위해 외래 키를 설정하는 것이 항상 좋은 선택이 아닐 수 있음
- 외래 키를 설정하면 데이터 무결성을 확인하는 추가 연산이 발생
- 무결성을 지켜야하기 때문에, 상황에 따라 개발하는데 불편할 수 있음 => 상황에 따라 가장 효율적인 제약조건 테이블에 적용하기
JDBC
- 실제로 서버가 데이터베이스와 소통하는 방법
- JDBC (Java Database Connectivity)
- DB에 접근할 수 있도록 Java에서 제공하는 API
- JDBC에 연결해야하는 DB의 JDBC 드라이버를 제공하면 DB 연결 로직을 변경할 필요없이 DB 변경이 가능함 - JDBC 드라이버: DB 회사들이 자신들의 DB에 맞도록 JDBC 인터페이스를 구현한 후 제공하는 라이브러리 -> ex) MySQL 드라이버를 사용해 DB에 연결을 하다 PostgreSQL 서버로 변경이 필요할 때 드라이버만 교체하면 손쉽게 DB 변경이 가능함
- JdbcTemplate
- 커넥션 연결, statement 준비 및 실행, 커넥션 종료 등의 반복적이고 중복되는 작업들을 대신 처리해줌 <- JDBC의 등장으로 손쉽게 DB교체가 가능해졌지만 DB에 연결하기 위해 여러가지 작업 로직을 직접 작성해야한다는 불편한 남았기 때문
=> JdbcTemplate이 JDBC를 직접 사용할 때 발생하는 불편함을 해결해주었지만, 여전히 복잡하고 사용하기 까다롭기 때문에 Java 개발자들을 위해 DB와 객체를 매핑하여 소통할 수 있는 ORM이라는 기술이 등장함
Layer Architecture
- 이전 메모장 프로젝트의 문제점: Controller 클래스 하나로 모든 API를 처리하고 있다.
- 당장은 API 수가 적고 기능이 단순해 코드가 복잡해 보이지 않지만, 앞으로 기능이 추가되고 복잡해진다면 문제가 발생할 수 있다.
Spring의 3 Layer Architecture
- 서버에서의 처리 과정이 대부분 비슷하다는 점을 이용해, 처리 과정을 크게 Controller, Service, Repository 3개로 분리함
- 당장은 API 수가 적고 기능이 단순해 코드가 복잡해 보이지 않지만, 앞으로 기능이 추가되고 복잡해진다면 문제가 발생할 수 있다.
- Controller
- 클라이언트의 요청을 받음, Service에서 처리 완료된 결과를 클라이언트에게 응답함
- 요청에 대한 로직 처리는 Service에게 전담
- Request 데이터가 있다면 Service에 같이 전달함
- Service
- 사용자의 요구사항을 처리(‘비즈니스 로직’)하는 실세
- DB 저장 및 조회가 필요할 때는 Repository에게 요청함
- Repository
- DB 관리(연결, 해제, 자원 관리)
- DB CRUD 작업 처리
IoC(제어의 역전), DI(의존성 주입)
- 객체지향의 SOLID 원칙 그리고 GoF의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴 ex) 김치 볶음밥을 맛있게 만드는 방법 - 설계 원칙 / 김치 볶음밥 레시피 - 디자인 패턴
좋은 코드를 위한 Spring의 IoC와 DI
- 좋은 코드란: 논리가 간단, 중복 제거하고 표현 명확, 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야함, 의존성 최소화, 새 기능 추가하더라도 크게 구조 변경 없어야함, …
- IoC와 DI는 좋은 코드 작성을 위한 Spring의 핵심 기술 중 하나임
- DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다.
의존성
- DI를 이해하기 위해 의존성이해 필요
강한 결합 ``` java public class Consumer {
void eat() { Chicken chicken = new Chicken(); chicken.eat(); }
public static void main(String[] args) { Consumer consumer = new Consumer(); consumer.eat(); } }
class Chicken { public void eat() { System.out.println(“치킨을 먹는다.”); } } ``` 강하게 결합되어있는 Consumer과 Chicken - Consumer이 치킨이 아니라 피자를 먹고 싶어 한다면, 많은 수의 코드 변경이 불가피함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
```
public class Consumer {
void eat(Food food) {
food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat(new Chicken());
consumer.eat(new Pizza());
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
```
Java의 Interface를 활용해 해결
- Interface 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처할 수 있음
- 주입
- 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것
필드에 직접 주입 ``` java public class Consumer {
Food food;
void eat() { this.food.eat(); }
public static void main(String[] args) { Consumer consumer = new Consumer(); consumer.food = new Chicken(); consumer.eat();
1 2
consumer.food = new Pizza(); consumer.eat(); } }
interface Food { void eat(); }
class Chicken implements Food{ @Override public void eat() { System.out.println(“치킨을 먹는다.”); } }
class Pizza implements Food{ @Override public void eat() { System.out.println(“피자를 먹는다.”); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Food를 Consumer에 포함시키고, Food에 필요한 객체를 주입 받아 사용할 수 있음
- 메서드를 통한 주입
``` java
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public void setFood(Food food) {
this.food = food;
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.setFood(new Chicken());
consumer.eat();
consumer.setFood(new Pizza());
consumer.eat();
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
1
set 메서드를 사용하여 필요한 객체를 주입받아 사용할 수 있음
생성자를 통한 주입 ``` java public class Consumer {
Food food;
public Consumer(Food food) { this.food = food; }
void eat() { this.food.eat(); }
public static void main(String[] args) { Consumer consumer = new Consumer(new Chicken()); consumer.eat();
1 2
consumer = new Consumer(new Pizza()); consumer.eat(); } }
interface Food { void eat(); }
class Chicken implements Food{ @Override public void eat() { System.out.println(“치킨을 먹는다.”); } }
class Pizza implements Food{ @Override public void eat() { System.out.println(“피자를 먹는다.”); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
생성자를 사용하여 필요한 객체를 주입받아 사용할 수 있음
> **제어의 역전**
- 이전에는 Consumer가 직접 Food를 만들어 먹었기 때문에 새로운 Food를 만들려면 추가적인 요리준비(코드변경)가 불가피했음
-> 이때는 제어의 흐름이 Consumer -> Food 였음
- 이를 해결하기 위해 만들어진 Food를 Consumer에게 전달해주는 식으로 변경함으로써 Consumer는 추가적인 요리 준비(코드변경) 없이 어느 Food가 되었든지 전부 먹을 수 있게됨
-> 결과적으로 제어의 흐름이 Food -> Consumer로 역전됨
(실제 세계에서도, 고객이 음식을 만드는 것이 아니라, 만들어진 음식이 고객에게 전달되는 것이기 때문)
#### 강한 결합 상태의 메모장 프로젝트
- 강한 결합이 존재하는 메모장 프로젝트
1. Controller가 Service 객체를 생성하여 사용
``` java
public class Controller1 {
private final Service1 service1;
public Controller1() {
this.service1 = new Service1();
}
}
. 2. Service가 Repository 객체를 생성하여 사용 3. Repository 객체 선언
강한 결합의 문제점:
- 5개의 Controller가 각각 Service1을 생성하여 사용함
- Repository1 생성자 변경에 의해 => 모든 Controller와 모든 Service의 코드 변경이 필요함
MemoService에서 자신은 사용하지 않지만 MemoRepository를 사용하기 위해 MemoRepository의 생성자에 JdbcTemplate을 넣어주고 있음 (MemoController에서도 마찬가지)
강한 결합 해결 방법
- 각 객체에 대한 객체 생성은 딱 1번만
- 생성된 객체를 모든 곳에서 재사용
- 생성자 주입을 사용하여 필요로하는 객체에 해당 객체 주입
.
- Repository1 클래스 선언 및 객체 생성 -> repository1 ``` java public class Repository1 { … }
// 객체 생성 Repository1 repository1 = new Repository1();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- Service1 클래스 선언 및 객체 생성 (repository1 사용) -> service1
``` java
Class Service1 {
private final Repository1 repitory1;
// repository1 객체 사용
public Service1(Repository1 repository1) {
this.repository1 = new Repository1();
this.repository1 = repository1;
}
}
// 객체 생성
Service1 service1 = new Service1(repository1);
- Controller1 선언 (service1 사용)
1 2 3 4 5 6 7 8 9
Class Controller1 { private final Service1 service1; // service1 객체 사용 public Controller1(Service1 service1) { this.service1 = new Service1(); this.service1 = service1; } }
=> 개선 결과:
- Repository1, Service1 생성자 변경은 다른 곳에 영향을 주지 않음 => 느슨한 결합
- 제어의 역전 (IoC: Inversion of Control)
- 강한 결합 상태의 메모장 프로젝트는 비효율적 (제어의 흐름: Controller -> Service -> Repository)
- 하지만, DI(의존성 주입)를 통해 제어의 흐름을 Repository -> Service -> Controller 로 역전함으로써 효율적 코드로 바꿈