DTO (Data Transfer Object)
: 데이터 전송 및 이동을 위해 생성되는 객체
- Client에서 보내오는 데이터를 객체로 처리할 때 사용됨
- 서버의 계층간의 이동에 사용된
- DB와의 소통을 담당하는 Java 클래스를 그대로 Client에 반환하는 것이 아니라 DTO로 한번 변환한 후 반환할 때도 사용됨
Database
: 데이터의 집합
- DBMS (Database Management System): Database를 관리하고 운영하는 소프트웨어
- RDBMS (Relational DBMS): 관계형 데이터베이스
ex) Oracle, MySQL
- table이라는 최소 단위로 구성, 각 table은 열(column)과 행(row)으로 이루어져있음
JOIN 이해하기
- 나누어진 테이블을 하나로 합치기 위해 데이터베이스가 제공하는 기능
ON이라는 키워드를 통해 기준이 되는 컬럼을 선택하여 2개의 테이블을 합쳐줌- JOIN 할 때는 적어도 하나의 컬럼을 서로 공유하고 있어야 하기 때문에, 테이블에 외래 키가 설정되어있다면 해당 컬럼을 통해 JOIN을 하면 조건을 충족할 수 있음
다만, JOIN을 하기 위해 외래 키를 설정하는 것이 항상 좋은 선택이 아닐 수 있음
- 외래 키를 설정하면 데이터 무결성을 확인하는 추가 연산이 발생
- 무결성을 지켜야하기 때문에, 상황에 따라 개발하는데 불편할 수 있음
=> 상황에 따라 가장 효율적인 제약조건 테이블에 적용하기
JDBC (Java Database Connectivity)
: DB에 접근할 수 있도록 Java에서 제공하는 API
JdbcTemplate
: 커넥션 연결, statement 준비 및 실행, 커넥션 종료 등의 반복적이고 중복되는 작업들을 대신 처리해줌
<- JDBC의 등장으로 손쉽게 DB교체가 가능해졌지만 DB에 연결하기 위해 여러가지 작업 로직을 직접 작성해야한다는 불편한 남았기 때문
=> JdbcTemplate이 JDBC를 직접 사용할 때 발생하는 불편함을 해결해주었지만, 여전히 복잡하고 사용하기 까다롭기 때문에 Java 개발자들을 위해 DB와 객체를 매핑하여 소통할 수 있는 ORM이라는 기술이 등장함
Spring의 3 Layer Architecture
- 서버에서의 처리 과정이 대부분 비슷하다는 점을 이용해, 처리 과정을 크게 Controller, Service, Repository 3개로 분리함
Controller
: 클라이언트의 요청을 받음, Service에서 처리 완료된 결과를 클라이언트에게 응답함
- 요청에 대한 로직 처리는 Service에게 전담
- Request 데이터가 있다면 Service에 같이 전달함
Service
: 사용자의 요구사항을 처리('비즈니스 로직')하는 실세
- DB 저장 및 조회가 필요할 때는 Repository에게 요청함
Repository
: DB 관리(연결, 해제, 자원 관리)
- DB CRUD 작업 처리
의존성
- DI를 이해하기 위해 의존성이해 필요
강한 결합
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이 치킨이 아니라 피자를 먹고 싶어 한다면, 많은 수의 코드 변경이 불가피함
```
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 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처할 수 있음
주입
: 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것
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();
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("피자를 먹는다.");
}
}
Food를 Consumer에 포함시키고, Food에 필요한 객체를 주입 받아 사용할 수 있음
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("피자를 먹는다.");
}
}
set 메서드를 사용하여 필요한 객체를 주입받아 사용할 수 있음
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();
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("피자를 먹는다.");
}
}
생성자를 사용하여 필요한 객체를 주입받아 사용할 수 있음
제어의 역전
- 이전에는 Consumer가 직접 Food를 만들어 먹었기 때문에 새로운 Food를 만들려면 추가적인 요리준비(코드변경)가 불가피했음
-> 이때는 제어의 흐름이 Consumer -> Food 였음- 이를 해결하기 위해 만들어진 Food를 Consumer에게 전달해주는 식으로 변경함으로써 Consumer는 추가적인 요리 준비(코드변경) 없이 어느 Food가 되었든지 전부 먹을 수 있게됨
-> 결과적으로 제어의 흐름이 Food -> Consumer로 역전됨
(실제 세계에서도, 고객이 음식을 만드는 것이 아니라, 만들어진 음식이 고객에게 전달되는 것이기 때문)
강한 결합이 존재하는 메모장 프로젝트
1. Controller가 Service 객체를 생성하여 사용
public class Controller1 {
private final Service1 service1;
public Controller1() {
this.service1 = new Service1();
}
}
.
2. Service가 Repository 객체를 생성하여 사용
3. Repository 객체 선언
강한 결합의 문제점:
MemoService에서 자신은 사용하지 않지만 MemoRepository를 사용하기 위해 MemoRepository의 생성자에 JdbcTemplate을 넣어주고 있음 (MemoController에서도 마찬가지)
강한 결합 해결 방법
Repository1 클래스 선언 및 객체 생성 -> repository1
public class Repository1 { ... }
// 객체 생성
Repository1 repository1 = new Repository1();
Service1 클래스 선언 및 객체 생성 (repository1 사용) -> service1
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 사용)
Class Controller1 {
private final Service1 service1;
// service1 객체 사용
public Controller1(Service1 service1) {
this.service1 = new Service1();
this.service1 = service1;
}
}
=> 개선 결과:
- Repository1, Service1 생성자 변경은 다른 곳에 영향을 주지 않음
=> 느슨한 결합
제어의 역전 (IoC: Inversion of Control)