개발/Spring

[Spring 입문] 3 layer architecture / IoC와 DI

서해쭈꾸미 2024. 5. 17. 01:18
3 layer architecture가 필요한 이유

 

Controller 클래스 하나로 모든 API를 처리한다면, 한 개의 클래스에 너무 많은 양의 코드가 존재하기 때문에 코드를 이해하기 어렵고 코드의 추가 혹은 변경 요청이 계속 생길 수 있기에 역할을 분리해줄 필요가 있다.

 

controller(출입문, 출구) ←→ service(사용자 요구사항 (logic) 처리) ←→ repository (DB 저장, 조회 필요할 때)

 

전체 흐름

 

Client ←(Request,Response)→ controller ←→ service ←→ repository ←→ DB

 

 

IoC와 DI

IoC(설계원칙 = 제어의 역전 ), DI(디자인 패턴 = 의존성 주입 ) 

 

 

의존성

 

강하게 결합된 Consumer와 Chicken

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 가 치킨이 아니라 피자를 먹고 싶어 한다면? 많은 수의 코드 변경이 불가피하다.

 

 

강하게 의존되어 있는 것을 해결하기 위해선 Interface의 다형성의 원리를 활용하면 약한 의존성으로 만들 수 있다.

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("피자를 먹는다.");
    }
}

 

주입

 

1.필드를 통한 주입

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("피자를 먹는다.");
    }
}

 

2.메서드를 통한 주입

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("피자를 먹는다.");
    }
}

 

3.생성자를 통한 주입

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로 역전된 것을 알 수 있다. 이제는 추가적인 코드변경 없이 Consumer가 어느 Food든 전부 먹을 수 있게 되었다.

 

즉, DI를 통해 IoC를 이루어냈다는 것을 알 수 있다.