포스트

[TIL] 스프링 입문 - DTO와 계층형 아키텍처

DTO의 역할과 Entity 대신 DTO를 통해 데이터를 주고받는 이유를 정리한 TIL입니다.

For the English version of this post, see here.
[TIL] 스프링 입문 - DTO와 계층형 아키텍처

오늘 할 일

공부한 내용

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개로 분리함
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. 각 객체에 대한 객체 생성은 딱 1번만
    2. 생성된 객체를 모든 곳에서 재사용
    3. 생성자 주입을 사용하여 필요로하는 객체에 해당 객체 주입

.

  • 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 로 역전함으로써 효율적 코드로 바꿈

문제 & 오류

내일 할 일