[Spring]3 Layer Architecture, IoC와 DI

김세림·2024년 5월 20일

Spring

목록 보기
4/9
post-thumbnail

3 Layer Architecture, IoC와 DI


Spring의 3 Layer Architecture

1. Controller

  • 클라이언트의 요청을 받습니다.
  • 요청에 대한 로직 처리는 Service에게 전담한다.
    • Request 데이터가 있다면 Service에 같이 전달한다.
  • Service에서 처리 완료된 결과를 클라이언트에게 응답한다.

2. Service

  • 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세 중에 실세다!
    • 따라서 현업에서는 서비스 코드가 계속 비대해진다.
  • DB 저장 및 조회가 필요할 때는 Repository에게 요청한다.

3. Repository

  • DB 관리 (연결, 해제, 자원 관리)
  • DB CRUD 작업을 처리한다.

이에 따른 전체적인 흐름이다.

IoC와 DI

Spring의 IoC와 DI

IoC, DI는 객체지향의 SOLID 원칙 그리고 GoF 의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴을 말한다.
이 둘을 더 자세하게 구분해보자면 IoC는 설계원칙에 해당하고 DI는 디자인 패턴에 해당한다.
IoC는 제어의 역전, DI는 의존성 주입을 뜻한다.

이를 김치볶음밥에 비유를 해보자면 아래와 같다.

김치 볶음밥 맛있게 만드는 방법 (설계 원칙)

🧑‍🍳 맛있는 김치 볶음밥을 만들기 위한 원칙
  • 신선한 재료를 사용한다.
  • 신 김치를 사용한다.
  • 밥과 김치의 비율을 잘 맞춰야 한다.
  • 볶을 때 재료의 순서가 중요하다.

김치 볶음밥 레시피 (디자인 패턴)

🍳 맛있는 김치 볶음밥을 만들기 위한 황금 레시피
  1. 오일을 두른 팬에 채썬 파를 볶아 파기름을 만든다.
  2. 준비한 햄을 넣고 볶다가, 간장 한스푼을 넣어 풍미를 낸다.
  3. 설탕에 버무린 김치를 넣고 함께 볶는다.
  4. 미리 식혀 둔 밥을 넣어 함께 볶는다.
  5. 참기름 한스푼을 넣어 마무리한다.

이 처럼 두개의 차이를 간략하게나마 볼 수 있을 것이다.

위는 Spring Docs에서 발췌해온 부분인데, 핵심 기술을 소개하는 Docs에서 가장 처음으로 IoC컨테이너에 대해서 설명하고 있다는 것을 보아서는 좋은 코드 작성을 위한 Spring의 핵심 기술 중 하나라는 것을 알 수 있을 것이다.

또한 위 내용에서 Ioc에 대해 'IoC는 DI로도 알려져 있다' 라고 소개하고 있는데 이부분은 DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다 라고 이해하면 좋을 것 같다.

의존성이란?

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을 먹는 메서드에서 직접 치킨을 만들어서 먹는 것을 볼 수 있다. 이는 강하게 결합하고 있다 라고 표현이 가능하다.

만약 피자를 먹고싶다면 다시 eat이라는 메서드에 피자를 만들어서 먹는 코드를 짜야할것이다.

이럴 때 결합을 약하게 하는 방법은 Java의 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("피자를 먹는다.");
    }
}

이처럼 interface 다형성의 원리를 사용하여 구현하면, 고객이 어떠한 음식을 요구하더라도 쉽게 대처할 수 있게된다.
이러한 관계를 약한 결합 및 약한 의존성이라고 할 수 있다.

주입이란?

그렇다면 의존성의 '주입' 이라고 했는데 주입은 무엇을 말하는 것일까?
우리가 주사기를 통해 백신을 몸속에 주입하듯이 코드에서도 여러방법을 통해 필요로하는 객체를 해당 객체에 전달하는 것을 의미한다.

또한 3가지의 방법이 있는데 이에 대해 설명해보겠다.

필드에 직접 주입

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

이처럼 생성자를 사용하여 필요한 객체를 주입받아 사용할 수 있다.

IoC(제어의 역전)이란?

위의 예제에서 이전에 강한결합일 때에는 Consumer가 직접 Food를 만들어 먹었기 때문에 새로운 Food를 만들려면 추가적인 코드변경을 해야했는데 이때는 제어의 흐름이 Consumer -> Food였다고 말할 수 있다.
이를 해결하기 위해 만들어진 Food를 Consumer에게 전달해주는 약한 결합으로 바꾸게 되었는데 이 결과 제어의 흐름이 Food -> Consumer로 역전이 되었다.

이를 제어의 역전이라고 한다.

IoC Container와 Bean

IoC Container

아까 위의 Spring Docs에서 IoC Container에 대해서 나왔었는데 이에대해 정리해보겠다.
우선 DI를 사용하기 위해서는 객체 생성이 우선이 되어야했다. 그렇다면 언제? 누가? 어디서? 객체를 생성하는 것일까?
바로 Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신해준다.
여기서 Spring이 관리하는 객체를 빈(Bean) 이라고 부르며, IoC 컨테이너는 그 빈을 모아둔 컨테이너를 뜻한다.

Spring 'Bean' 등록방법

  1. @Component
  • Bean 으로 등록하고자하는 클래스 위에 해당 애너테이션을 달아준다.
@Component
public class MemoService { ... }
  • Spring 서버가 뜰 때 IoC 컨테이너에 Bean을 저장해준다.
// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너
  • Spring Bean의 이름은 클래스의 앞글자만 소문자로 변경된다.

  • Bean이 되었는지 확인은 클래스의 왼쪽에 커피콩 모양으로 아이콘이 생긴다면 제대로 생겼다는 것을 확인할 수 있다.

  1. @ComponentScan
  • Spring 서버가 뜰 때 @ComponentScan에 설정해 준 packages 위치와 하위 packages 들을 전부 확인하여 @Component가 설정된 클래스들을 ‘Bean’으로 등록 해준다.
@Configuration
@ComponentScan(basePackages = "com.sparta.memo")
class BeanConfig { ... }
  • @SpringBootApplication에 의해 default 설정 되어있다.
    com.sparta.memo/MemoApplication.java

Spring 'Bean' 사용방법

  1. @Autowired
  • 필드 혹은 메서드 위에 해당 애너테이션을 달면된다.
@Component
public class MemoService {

    private final MemoRepository memoRepository;

    @Autowired
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
}
  • Spring에서 IoC 컨테이너에 저장된 memoRepository ‘Bean’을 해당 필드에 DI 즉, 의존성을 주입한다.

  • 객체의 불변성을 확보할 수 있기 때문에 일반적으로는 생성자를 사용하여 DI하는 것이 좋다.

  • @Autowired 적용 조건

    • Spring IoC 컨테이너에 의해 관리되는 클래스에서만 가능하다. (Bean 처리되어있는 클래스)
  • @Autowired 생략 조건

    • Spring 4.3 버전부터 생략이 가능하지만, 생성자 선언이 1개일때만 생략이 가능하다.
      오버로딩이 되어있다면 생략이 불가능하다는 뜻이다.

    • Lombok의 @RequiredArgsConstructor를 사용하면 다음과 같이 코딩 가능하다.

      @Component
      @RequiredArgsConstructor 
      // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
      public class MemoService {
      
       private final MemoRepository memoRepository;
       
      	//    public MemoService(MemoRepository memoRepository) {
      	//        this.memoRepository = memoRepository;
      	//    }
      
      		...
      
      	}
  1. ApplicationContext
  • ApplicationContext는 BeanFactory등을 상속하여 기능을 확장한 Container이다.
    • BeanFactory는 ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체이다.
  • 스프링 IoC 컨테이너에서 ‘Bean’을 수동으로 가져오는 방법
@Component
public class MemoService {

		private final MemoRepository memoRepository;

    public MemoService(ApplicationContext context) {
        // 1.'Bean' 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

        // 2.'Bean' 클래스 형식으로 가져오기
        // MemoRepository memoRepository = context.getBean(MemoRepository.class);

        this.memoRepository = memoRepository;
    }

		...		
}

3 Layer Annotation

그런데 그동안 우리는 어떻게 빈을 만들지 않고 사용했을까?
바로 3Layer로 해두었던 우리의 애너테이션들 덕분이다.
1. @Controller, @RestController
2. @Service
3. @Repository
이 모두 Bean을 자동으로 만들어주는 Bean 클래스의 역할을 명시하기위해 사용되는 애너테이션이기 때문에 Component가 아닌 해당 애너테이션을 사용하여 Bean으로 등록할 수 있었던 것이다.

해당 애너테이션들은 @Component가 추가되어있는 상태인것이다.

0개의 댓글