Light Blue Pointer
본문 바로가기
TIL(Develop)

IoC와 DI

by 개발바닥곰발바닥!!! 2023. 11. 9.

IoC (제어의 역전) → 객체지향의 SOLID 설계원칙

DI (의존성 주입) → GoF 디자인 패턴

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

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

좋은 코드를 위한 Spring의 IoC와 DI

  • 논리가 간단해야 한다.
  • 중복을 제거하고 표현을 명확하게 한다.
  • 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야 한다.
  • 의존성을 최소화해야 한다.
  • 새로운 기능을 추가 하더라도 크게 구조의 변경이 없어야 한다.

→ DI 패턴을 사용하여 IoC 설계 원칙을 구현하여 Spring이 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와줌

의존성

강하게 결합되어있는 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 가 치킨이 아니라 피자를 먹고 싶어 하는 등

변경이 필요할 때

많은 수의 코드를 변경해야 함

→ 약한 결합, 약한 의존성으로 만들어봄

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 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처

주입

필요로 하는 객체를 해당 객체에 전달하는 것

필드에 직접 주입

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를 먹으려면 추가적인 코드변경이 필요했음

Food를 Consumer에게 주입해서 코드 변경 없이 어느 Food든 먹을 수 있게 됨

이를 제어의 흐름이 Food → Consumer 로 역전되었다고 한다

Consumer는 가만히 있고 외부에서 Food를 만들어서 주입하니까

메모장 프로젝트의 IoC & DI

하기 전에

! 잠깐 !

지금 메모장 프로젝트 코드가 가지고 있는 객체 중복 생성 문제를 먼저 해결

new MemoRepository(jdbcTemplate); 코드가 중복되고 있음

public class MemoService {

    private final JdbcTemplate jdbcTemplate;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
				...
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }
}

→ 객체가 생성될때 한번만 만들어서 같이 쓰면 훨씬 효율적

기존 코드

  	private final JdbcTemplate jdbcTemplate;
    
    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

		public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        Memo saveMemo = memoRepository.save(memo);**
				...
    }

바뀐 코드

public class MemoService {
   
    private final MemoRepository memoRepository;

		public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    
   public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **Memo saveMemo = memoRepository.save(requestDto);
	...**
     }
	...
}

MemoController의 이 부분도 개선

@RestController
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {

        MemoService memoService = new MemoService(jdbcTemplate);
  
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.deleteMemo(id);
    }
}

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        return memoService.deleteMemo(id);
    }
}

메모장 프로젝트의 강한 결합상태 개선

강한 결합 문제상황(예시)

  1. Contoller1Service1 객체를 생성하여 사용
public class Controller1 {
	private final Service1 service1;

	public Controller1() {
		this.service1 = new Service1();
	}
}
  1. Service1Repostiroy1 객체를 생성하여 사용
public class Service1 {
	private final Repository1 repository1;

	public Service1() {
		this.repository1 = new Repository1();
	}
}
  1. Repostiroy1 객체 선언
public class Repository1 { ... }

이 상황에서 다음과 같이 변경된다면 어떨까?

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

(생성자에 DB 접속 id, pw 를 추가)

public class Repository1 {

	public Repository1(String id, String pw) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", id, pw);
  }
}

강한 결합의 문제점

Controller 5 개가 각각 Service1 을 생성하여 사용 중일 때,

Repository1 생성자가 변경되면

모든 Contoller모든 Service 의 코드 변경이 필요함

강한 결합 해결방법

  1. 각 객체에 대한 객체 생성은 딱 1번만
  2. 생성된 객체를 모든 곳에서 재사용
  3. 필요한 객체에 생성자 주입으로 해당 객체 주입
  1. Repository1 클래스 선언 및 객체 생성repository1
public class Repository1 { ... }

// 객체 생성
**Repository1 repository1 = new Repository1();**
  1. Service1 클래스 선언 및 객체 생성 (repostiroy1 사용)service1
Class Service1 {
	private final Repository1 repository1;

	// repository1 객체 사용
	public Service1(Repository1 repository1) {
		~~this.repository1 = new Repository1();~~
		this.repository1 = repository1;
	}
}

// 객체 생성
**Service1 service1 = new Service1(repository1);**
  1. Contoller1 선언 ( service1 사용)
Class Controller1 {
	private final Service1 service1;

	// service1 객체 사용
	public Controller1(Service1 service1) {
		~~this.service1 = new Service1();~~
		this.service1 = service1;
	}
}

다음과 같이 변경됨 →

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

생성자에 DB 접속 id, pw 를 추가

public class Repository1 {

	public Repository1(**String id, String pw**) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", **id, pw**);
  }
}

// 객체 생성
**String id = "root";
String pw = "1234";**
Repository1 repository1 = new Repository1(**id, pw**);

[개선 결과]

Repository1 생성자 변경이 적은 코드 수정만으로 ok

Service1 생성자가 변경되면? 모든 Controller 변경 필요 X

문제 상황

!메모장 프로젝트에서도 동일한 문제 발생중이었음!

public MemoController(JdbcTemplate jdbcTemplate) {
    this.memoService = new MemoService(jdbcTemplate);
}

...

public MemoService(JdbcTemplate jdbcTemplate) {
    this.memoRepository = new MemoRepository(jdbcTemplate);
}

문제 상황

  1. MemoService에서 자신은 사용하지 않지만 MemoRepository를 사용하기 위해 MemoRepository의 생성자에 JdbcTemplate을 넣어주고 있다.
  2. MemoController에서도 MemoService를 사용해야하기 때문에 생성자에 JdbcTemplate을 넣어주고 있다.

코드 따라 짜면서 왜 jdbcTemplate넘기고 또 넘기는 형식으로 하는지 그러는 이유가 있었는지 궁금했었는데 여기서 풀리네…

문제 해결

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }
public class MemoService {
    private final JdbcTemplate jdbcTemplate;
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }

public class MemoService {
private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

이제는 더 이상 MemoController와 MemoService에서 사용하지도 않는 JdbcTemplate을 주입 받아와 MemoRepository애 주입해줄 필요가 없어졌다.

코드 수정하면서 제어의 흐름이 반대가 된다는게 이런 뜻인거 같다고 어렴풋이 느꼈다:

예전에는

Controller만들면 그 안에서 Service만들고 Service안에서 Repository만들었었는데

이제 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣음

반대가 됨

근데 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣는 걸

누가 언제 어디서 해주는거지? → 다음 강의

**IoC: Inversion of Control(**제어의 역전)

프로그램의 제어 흐름이 뒤바뀜

제어의 흐름 of 강한 결합 상태의 메모장 프로젝트

Controller ⇒ Service ⇒ Repository

Controller에서 Service를 만들고 Service에서 Repository를 만듦

Consumer가 직접 음식을 만들어 먹었을 때와 같다

제어의 흐름 of DI(의존성 주입)후 메모장 프로젝트

Repository ⇒ Service ⇒ Controller

Food가 만들어져서 Consumer에게 주입당했을때랑 같음

Spring IoC 컨테이너

DI를 사용하기 위해서는 객체를 우선 만들어야 함

그러면 누가 그걸 해줌?

Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해줌

Bean :

Spring이 관리하는 객체

Spring IoC 컨테이너 :

Bean을 모아둔 컨테이너

Spring Bean 등록 방법

@Component

‘Bean’으로 등록하고자하는 클래스 위에 설정

→Spring 서버가 뜰 때 IoC 컨테이너에 'Bean'을 저장

@Component
public class MemoService { ... }
@Component
public class MemoRepository {...}

MemoRepository에 @Component다니까 빨간 줄 사라짐

MemoApplication → @SpringBootApplication → ctrl + click → SpringBootApplication.java

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

@ComponentScan : Component를 찾아주는 annotation

이게 달린 패키지와 하위 패키지를 전부 확인한다고 함

Component annotation이 설정되어있는 클래스들을 전부 찾아서 Bean으로 등록을 한다고 함

@SpringBootApplication에 의해 default 설정 되어있습니다.

com.sparta.memo/MemoApplication.java

Spring 'Bean' 이름: 클래스의 앞글자만 소문자로 변경

public class MemoServicememoService

'Bean' 아이콘 → 스프링 IoC 에서 관리할 'Bean' 클래스라는 표시

// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너

Spring 'Bean' 사용 방법

@Autowired

필드 위에 @Autowired

@Component
public class MemoService {
		
    **@Autowired**
    private MemoRepository memoRepository;
		
		// ...
}

Spring에서 IoC 컨테이너에 저장된 memoRepository ‘Bean’을 해당 필드에 DI (의존성 주입)

메서드 위에 @Autowired

@Component
public class MemoService {

    private final MemoRepository memoRepository;

    **@Autowired**
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
		// ...
}

객체의 불변성을 확보하기 위해 → final로

생성자에 DI(@Autowired를 적용)하는 것이 좋다.

Setter로 DI(@Autowired) 할 시 final 못 씀

private에도 autowired 달면 Spring이 알아서 주입해줌, 추천하지 않는 방법

이유는 나중에 알게된다 함

강의에서는 그냥 생성자로 주입

@Autowired 적용 조건

Spring IoC 컨테이너에 의해 관리되는 클래스에서만 가능

Spring IoC 컨테이너에 의해 관리되는 ‘Bean’객체만 DI에 사용가능

@Autowired 생략 조건

Spring 4.3 버젼 부터 생성자 선언이 1개 일 때 @Autowired 생략가능

public class A {
//생략 가능
	public A(B b) { ... }
}

오버로딩된 생성자들

public class A {
	@Autowired // 생략 불가
	public A(B b) { ... }

	@Autowired // 생략 불가
	public A(B b, C c) { ... }
}

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

@Component
**@RequiredArgsConstructor** // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}
  1. ApplicationContextBeanFactory : ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체
  2. 스프링 IoC 컨테이너에서 ‘Bean’을 수동으로 가져오는 방법
  3. ApplicationContext는 BeanFactory등을 상속하여 기능을 확장한 Container
public MemoService(ApplicationContext context) {
        //1. Bean 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

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

3 Layer Annotation

  • Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용됩니다.
    1. @Controller, @RestController
    2. @Service
    3. @Repository
  • 앞으로는 @Component가 아닌 3 Layer Annotation을 사용해서 ‘Bean’으로 등록하겠습니다.

<aside> 💡 Spring 3 Layer Annotation은 모두 @Component가 추가되어있습니다

</aside>

IoC (제어의 역전) → 객체지향의 SOLID 설계원칙

DI (의존성 주입) → GoF 디자인 패턴

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

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

좋은 코드를 위한 Spring의 IoC와 DI

  • 논리가 간단해야 한다.
  • 중복을 제거하고 표현을 명확하게 한다.
  • 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야 한다.
  • 의존성을 최소화해야 한다.
  • 새로운 기능을 추가 하더라도 크게 구조의 변경이 없어야 한다.

→ DI 패턴을 사용하여 IoC 설계 원칙을 구현하여 Spring이 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와줌

의존성

강하게 결합되어있는 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 가 치킨이 아니라 피자를 먹고 싶어 하는 등

변경이 필요할 때

많은 수의 코드를 변경해야 함

→ 약한 결합, 약한 의존성으로 만들어봄

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 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처

주입

필요로 하는 객체를 해당 객체에 전달하는 것

필드에 직접 주입

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를 먹으려면 추가적인 코드변경이 필요했음

Food를 Consumer에게 주입해서 코드 변경 없이 어느 Food든 먹을 수 있게 됨

이를 제어의 흐름이 Food → Consumer 로 역전되었다고 한다

Consumer는 가만히 있고 외부에서 Food를 만들어서 주입하니까

메모장 프로젝트의 IoC & DI

하기 전에

! 잠깐 !

지금 메모장 프로젝트 코드가 가지고 있는 객체 중복 생성 문제를 먼저 해결

new MemoRepository(jdbcTemplate); 코드가 중복되고 있음

public class MemoService {

    private final JdbcTemplate jdbcTemplate;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
				...
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }
}

→ 객체가 생성될때 한번만 만들어서 같이 쓰면 훨씬 효율적

기존 코드

  	private final JdbcTemplate jdbcTemplate;
    
    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

		public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        Memo saveMemo = memoRepository.save(memo);**
				...
    }

바뀐 코드

public class MemoService {
   
    private final MemoRepository memoRepository;

		public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    
   public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **Memo saveMemo = memoRepository.save(requestDto);
	...**
     }
	...
}

MemoController의 이 부분도 개선

@RestController
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {

        MemoService memoService = new MemoService(jdbcTemplate);
  
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.deleteMemo(id);
    }
}

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        return memoService.deleteMemo(id);
    }
}

메모장 프로젝트의 강한 결합상태 개선

강한 결합 문제상황(예시)

  1. Contoller1Service1 객체를 생성하여 사용
public class Controller1 {
	private final Service1 service1;

	public Controller1() {
		this.service1 = new Service1();
	}
}
  1. Service1Repostiroy1 객체를 생성하여 사용
public class Service1 {
	private final Repository1 repository1;

	public Service1() {
		this.repository1 = new Repository1();
	}
}
  1. Repostiroy1 객체 선언
public class Repository1 { ... }

이 상황에서 다음과 같이 변경된다면 어떨까?

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

(생성자에 DB 접속 id, pw 를 추가)

public class Repository1 {

	public Repository1(String id, String pw) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", id, pw);
  }
}

강한 결합의 문제점

Controller 5 개가 각각 Service1 을 생성하여 사용 중일 때,

Repository1 생성자가 변경되면

모든 Contoller모든 Service 의 코드 변경이 필요함

강한 결합 해결방법

  1. 각 객체에 대한 객체 생성은 딱 1번만
  2. 생성된 객체를 모든 곳에서 재사용
  3. 필요한 객체에 생성자 주입으로 해당 객체 주입
  1. Repository1 클래스 선언 및 객체 생성repository1
public class Repository1 { ... }

// 객체 생성
**Repository1 repository1 = new Repository1();**
  1. Service1 클래스 선언 및 객체 생성 (repostiroy1 사용)service1
Class Service1 {
	private final Repository1 repository1;

	// repository1 객체 사용
	public Service1(Repository1 repository1) {
		~~this.repository1 = new Repository1();~~
		this.repository1 = repository1;
	}
}

// 객체 생성
**Service1 service1 = new Service1(repository1);**
  1. Contoller1 선언 ( service1 사용)
Class Controller1 {
	private final Service1 service1;

	// service1 객체 사용
	public Controller1(Service1 service1) {
		~~this.service1 = new Service1();~~
		this.service1 = service1;
	}
}

다음과 같이 변경됨 →

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

생성자에 DB 접속 id, pw 를 추가

public class Repository1 {

	public Repository1(**String id, String pw**) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", **id, pw**);
  }
}

// 객체 생성
**String id = "root";
String pw = "1234";**
Repository1 repository1 = new Repository1(**id, pw**);

[개선 결과]

Repository1 생성자 변경이 적은 코드 수정만으로 ok

Service1 생성자가 변경되면? 모든 Controller 변경 필요 X

문제 상황

!메모장 프로젝트에서도 동일한 문제 발생중이었음!

public MemoController(JdbcTemplate jdbcTemplate) {
    this.memoService = new MemoService(jdbcTemplate);
}

...

public MemoService(JdbcTemplate jdbcTemplate) {
    this.memoRepository = new MemoRepository(jdbcTemplate);
}

문제 상황

  1. MemoService에서 자신은 사용하지 않지만 MemoRepository를 사용하기 위해 MemoRepository의 생성자에 JdbcTemplate을 넣어주고 있다.
  2. MemoController에서도 MemoService를 사용해야하기 때문에 생성자에 JdbcTemplate을 넣어주고 있다.

코드 따라 짜면서 왜 jdbcTemplate넘기고 또 넘기는 형식으로 하는지 그러는 이유가 있었는지 궁금했었는데 여기서 풀리네…

문제 해결

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }
public class MemoService {
    private final JdbcTemplate jdbcTemplate;
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }

public class MemoService {
private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

이제는 더 이상 MemoController와 MemoService에서 사용하지도 않는 JdbcTemplate을 주입 받아와 MemoRepository애 주입해줄 필요가 없어졌다.

코드 수정하면서 제어의 흐름이 반대가 된다는게 이런 뜻인거 같다고 어렴풋이 느꼈다:

예전에는

Controller만들면 그 안에서 Service만들고 Service안에서 Repository만들었었는데

이제 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣음

반대가 됨

근데 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣는 걸

누가 언제 어디서 해주는거지? → 다음 강의

**IoC: Inversion of Control(**제어의 역전)

프로그램의 제어 흐름이 뒤바뀜

제어의 흐름 of 강한 결합 상태의 메모장 프로젝트

Controller ⇒ Service ⇒ Repository

Controller에서 Service를 만들고 Service에서 Repository를 만듦

Consumer가 직접 음식을 만들어 먹었을 때와 같다

제어의 흐름 of DI(의존성 주입)후 메모장 프로젝트

Repository ⇒ Service ⇒ Controller

Food가 만들어져서 Consumer에게 주입당했을때랑 같음

Spring IoC 컨테이너

DI를 사용하기 위해서는 객체를 우선 만들어야 함

그러면 누가 그걸 해줌?

Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해줌

Bean :

Spring이 관리하는 객체

Spring IoC 컨테이너 :

Bean을 모아둔 컨테이너

Spring Bean 등록 방법

@Component

‘Bean’으로 등록하고자하는 클래스 위에 설정

→Spring 서버가 뜰 때 IoC 컨테이너에 'Bean'을 저장

@Component
public class MemoService { ... }
@Component
public class MemoRepository {...}

MemoRepository에 @Component다니까 빨간 줄 사라짐

MemoApplication → @SpringBootApplication → ctrl + click → SpringBootApplication.java

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

@ComponentScan : Component를 찾아주는 annotation

이게 달린 패키지와 하위 패키지를 전부 확인한다고 함

Component annotation이 설정되어있는 클래스들을 전부 찾아서 Bean으로 등록을 한다고 함

@SpringBootApplication에 의해 default 설정 되어있습니다.

com.sparta.memo/MemoApplication.java

Spring 'Bean' 이름: 클래스의 앞글자만 소문자로 변경

public class MemoServicememoService

'Bean' 아이콘 → 스프링 IoC 에서 관리할 'Bean' 클래스라는 표시

// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너

Spring 'Bean' 사용 방법

@Autowired

필드 위에 @Autowired

@Component
public class MemoService {
		
    **@Autowired**
    private MemoRepository memoRepository;
		
		// ...
}

Spring에서 IoC 컨테이너에 저장된 memoRepository ‘Bean’을 해당 필드에 DI (의존성 주입)

메서드 위에 @Autowired

@Component
public class MemoService {

    private final MemoRepository memoRepository;

    **@Autowired**
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
		// ...
}

객체의 불변성을 확보하기 위해 → final로

생성자에 DI(@Autowired를 적용)하는 것이 좋다.

Setter로 DI(@Autowired) 할 시 final 못 씀

private에도 autowired 달면 Spring이 알아서 주입해줌, 추천하지 않는 방법

이유는 나중에 알게된다 함

강의에서는 그냥 생성자로 주입

@Autowired 적용 조건

Spring IoC 컨테이너에 의해 관리되는 클래스에서만 가능

Spring IoC 컨테이너에 의해 관리되는 ‘Bean’객체만 DI에 사용가능

@Autowired 생략 조건

Spring 4.3 버젼 부터 생성자 선언이 1개 일 때 @Autowired 생략가능

public class A {
//생략 가능
	public A(B b) { ... }
}

오버로딩된 생성자들

public class A {
	@Autowired // 생략 불가
	public A(B b) { ... }

	@Autowired // 생략 불가
	public A(B b, C c) { ... }
}

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

@Component
**@RequiredArgsConstructor** // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}
  1. ApplicationContextBeanFactory : ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체
  2. 스프링 IoC 컨테이너에서 ‘Bean’을 수동으로 가져오는 방법
  3. ApplicationContext는 BeanFactory등을 상속하여 기능을 확장한 Container
public MemoService(ApplicationContext context) {
        //1. Bean 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

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

3 Layer Annotation

  • Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용됩니다.
    1. @Controller, @RestController
    2. @Service
    3. @Repository
  • 앞으로는 @Component가 아닌 3 Layer Annotation을 사용해서 ‘Bean’으로 등록하겠습니다.

<aside> 💡 Spring 3 Layer Annotation은 모두 @Component가 추가되어있습니다

</aside>

IoC (제어의 역전) → 객체지향의 SOLID 설계원칙

DI (의존성 주입) → GoF 디자인 패턴

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

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

좋은 코드를 위한 Spring의 IoC와 DI

  • 논리가 간단해야 한다.
  • 중복을 제거하고 표현을 명확하게 한다.
  • 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야 한다.
  • 의존성을 최소화해야 한다.
  • 새로운 기능을 추가 하더라도 크게 구조의 변경이 없어야 한다.

→ DI 패턴을 사용하여 IoC 설계 원칙을 구현하여 Spring이 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와줌

의존성

강하게 결합되어있는 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 가 치킨이 아니라 피자를 먹고 싶어 하는 등

변경이 필요할 때

많은 수의 코드를 변경해야 함

→ 약한 결합, 약한 의존성으로 만들어봄

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 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처

주입

필요로 하는 객체를 해당 객체에 전달하는 것

필드에 직접 주입

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를 먹으려면 추가적인 코드변경이 필요했음

Food를 Consumer에게 주입해서 코드 변경 없이 어느 Food든 먹을 수 있게 됨

이를 제어의 흐름이 Food → Consumer 로 역전되었다고 한다

Consumer는 가만히 있고 외부에서 Food를 만들어서 주입하니까

메모장 프로젝트의 IoC & DI

하기 전에

! 잠깐 !

지금 메모장 프로젝트 코드가 가지고 있는 객체 중복 생성 문제를 먼저 해결

new MemoRepository(jdbcTemplate); 코드가 중복되고 있음

public class MemoService {

    private final JdbcTemplate jdbcTemplate;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
				...
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }
}

→ 객체가 생성될때 한번만 만들어서 같이 쓰면 훨씬 효율적

기존 코드

  	private final JdbcTemplate jdbcTemplate;
    
    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

		public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        Memo saveMemo = memoRepository.save(memo);**
				...
    }

바뀐 코드

public class MemoService {
   
    private final MemoRepository memoRepository;

		public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    
   public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        **Memo saveMemo = memoRepository.save(requestDto);
	...**
     }
	...
}

MemoController의 이 부분도 개선

@RestController
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {

        MemoService memoService = new MemoService(jdbcTemplate);
  
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.deleteMemo(id);
    }
}

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        return memoService.createMemo(requestDto);//클라이언트에서 전달받은 requestDto도 Service에게 사용하라고 보내줘야 함
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        return memoService.getMemos();
    }

    @PutMapping("/memos/{id}")
    public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
        return memoService.updateMemo(id,requestDto);
    }

    @DeleteMapping("/memos/{id}")
    public Long deleteMemo(@PathVariable Long id) {
        return memoService.deleteMemo(id);
    }
}

메모장 프로젝트의 강한 결합상태 개선

강한 결합 문제상황(예시)

  1. Contoller1Service1 객체를 생성하여 사용
public class Controller1 {
	private final Service1 service1;

	public Controller1() {
		this.service1 = new Service1();
	}
}
  1. Service1Repostiroy1 객체를 생성하여 사용
public class Service1 {
	private final Repository1 repository1;

	public Service1() {
		this.repository1 = new Repository1();
	}
}
  1. Repostiroy1 객체 선언
public class Repository1 { ... }

이 상황에서 다음과 같이 변경된다면 어떨까?

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

(생성자에 DB 접속 id, pw 를 추가)

public class Repository1 {

	public Repository1(String id, String pw) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", id, pw);
  }
}

강한 결합의 문제점

Controller 5 개가 각각 Service1 을 생성하여 사용 중일 때,

Repository1 생성자가 변경되면

모든 Contoller모든 Service 의 코드 변경이 필요함

강한 결합 해결방법

  1. 각 객체에 대한 객체 생성은 딱 1번만
  2. 생성된 객체를 모든 곳에서 재사용
  3. 필요한 객체에 생성자 주입으로 해당 객체 주입
  1. Repository1 클래스 선언 및 객체 생성repository1
public class Repository1 { ... }

// 객체 생성
**Repository1 repository1 = new Repository1();**
  1. Service1 클래스 선언 및 객체 생성 (repostiroy1 사용)service1
Class Service1 {
	private final Repository1 repository1;

	// repository1 객체 사용
	public Service1(Repository1 repository1) {
		~~this.repository1 = new Repository1();~~
		this.repository1 = repository1;
	}
}

// 객체 생성
**Service1 service1 = new Service1(repository1);**
  1. Contoller1 선언 ( service1 사용)
Class Controller1 {
	private final Service1 service1;

	// service1 객체 사용
	public Controller1(Service1 service1) {
		~~this.service1 = new Service1();~~
		this.service1 = service1;
	}
}

다음과 같이 변경됨 →

Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용

생성자에 DB 접속 id, pw 를 추가

public class Repository1 {

	public Repository1(**String id, String pw**) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", **id, pw**);
  }
}

// 객체 생성
**String id = "root";
String pw = "1234";**
Repository1 repository1 = new Repository1(**id, pw**);

[개선 결과]

Repository1 생성자 변경이 적은 코드 수정만으로 ok

Service1 생성자가 변경되면? 모든 Controller 변경 필요 X

문제 상황

!메모장 프로젝트에서도 동일한 문제 발생중이었음!

public MemoController(JdbcTemplate jdbcTemplate) {
    this.memoService = new MemoService(jdbcTemplate);
}

...

public MemoService(JdbcTemplate jdbcTemplate) {
    this.memoRepository = new MemoRepository(jdbcTemplate);
}

문제 상황

  1. MemoService에서 자신은 사용하지 않지만 MemoRepository를 사용하기 위해 MemoRepository의 생성자에 JdbcTemplate을 넣어주고 있다.
  2. MemoController에서도 MemoService를 사용해야하기 때문에 생성자에 JdbcTemplate을 넣어주고 있다.

코드 따라 짜면서 왜 jdbcTemplate넘기고 또 넘기는 형식으로 하는지 그러는 이유가 있었는지 궁금했었는데 여기서 풀리네…

문제 해결

public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        memoService = new MemoService(jdbcTemplate);
    }

public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }
public class MemoService {
    private final JdbcTemplate jdbcTemplate;
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }

public class MemoService {
private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

이제는 더 이상 MemoController와 MemoService에서 사용하지도 않는 JdbcTemplate을 주입 받아와 MemoRepository애 주입해줄 필요가 없어졌다.

코드 수정하면서 제어의 흐름이 반대가 된다는게 이런 뜻인거 같다고 어렴풋이 느꼈다:

예전에는

Controller만들면 그 안에서 Service만들고 Service안에서 Repository만들었었는데

이제 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣음

반대가 됨

근데 Repository를 만들어서 Service생성자에 넣고 그걸 또 Controller생성자에 넣는 걸

누가 언제 어디서 해주는거지? → 다음 강의

IoC: Inversion of Control(제어의 역전)

프로그램의 제어 흐름이 뒤바뀜

제어의 흐름 of 강한 결합 상태의 메모장 프로젝트

Controller ⇒ Service ⇒ Repository

Controller에서 Service를 만들고 Service에서 Repository를 만듦

Consumer가 직접 음식을 만들어 먹었을 때와 같다

제어의 흐름 of DI(의존성 주입)후 메모장 프로젝트

Repository ⇒ Service ⇒ Controller

Food가 만들어져서 Consumer에게 주입당했을때랑 같음

Spring IoC 컨테이너

DI를 사용하기 위해서는 객체를 우선 만들어야 함

그러면 누가 그걸 해줌?

Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해줌

Bean :

Spring이 관리하는 객체

Spring IoC 컨테이너 :

Bean을 모아둔 컨테이너

Spring Bean 등록 방법

@Component

‘Bean’으로 등록하고자하는 클래스 위에 설정

→Spring 서버가 뜰 때 IoC 컨테이너에 'Bean'을 저장

@Component
public class MemoService { ... }

 

@Component
public class MemoRepository {...}

 

MemoRepository에 @Component다니까 빨간 줄 사라짐

MemoApplication → @SpringBootApplication → ctrl + click → SpringBootApplication.java

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

@ComponentScan : Component를 찾아주는 annotation

이게 달린 패키지와 하위 패키지를 전부 확인한다고 함

Component annotation이 설정되어있는 클래스들을 전부 찾아서 Bean으로 등록을 한다고 함

@SpringBootApplication에 의해 default 설정 되어있습니다.

com.sparta.memo/MemoApplication.java

Spring 'Bean' 이름: 클래스의 앞글자만 소문자로 변경

public class MemoServicememoService

'Bean' 아이콘 → 스프링 IoC 에서 관리할 'Bean' 클래스라는 표시

// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너

Spring 'Bean' 사용 방법

@Autowired

필드 위에 @Autowired

@Component
public class MemoService {
		
    **@Autowired**
    private MemoRepository memoRepository;
		
		// ...
}

Spring에서 IoC 컨테이너에 저장된 memoRepository ‘Bean’을 해당 필드에 DI (의존성 주입)

메서드 위에 @Autowired

@Component
public class MemoService {

    private final MemoRepository memoRepository;

    **@Autowired**
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
		// ...
}

객체의 불변성을 확보하기 위해 → final로

생성자에 DI(@Autowired를 적용)하는 것이 좋다.

Setter로 DI(@Autowired) 할 시 final 못 씀

private에도 autowired 달면 Spring이 알아서 주입해줌, 추천하지 않는 방법

이유는 나중에 알게된다 함

강의에서는 그냥 생성자로 주입

@Autowired 적용 조건

Spring IoC 컨테이너에 의해 관리되는 클래스에서만 가능

Spring IoC 컨테이너에 의해 관리되는 ‘Bean’객체만 DI에 사용가능

@Autowired 생략 조건

Spring 4.3 버젼 부터 생성자 선언이 1개 일 때 @Autowired 생략가능

public class A {
//생략 가능
	public A(B b) { ... }
}

오버로딩된 생성자들

public class A {
	@Autowired // 생략 불가
	public A(B b) { ... }

	@Autowired // 생략 불가
	public A(B b, C c) { ... }
}

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

@Component
**@RequiredArgsConstructor** // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}
  1. ApplicationContextBeanFactory : ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체
  2. 스프링 IoC 컨테이너에서 ‘Bean’을 수동으로 가져오는 방법
  3. ApplicationContext는 BeanFactory등을 상속하여 기능을 확장한 Container
public MemoService(ApplicationContext context) {
        //1. Bean 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

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

3 Layer Annotation

  • Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용됩니다.
    1. @Controller, @RestController
    2. @Service
    3. @Repository
  • 앞으로는 @Component가 아닌 3 Layer Annotation을 사용해서 ‘Bean’으로 등록하겠습니다.

💡 Spring 3 Layer Annotation은 모두 @Component가 추가되어있습니다

 

'TIL(Develop)' 카테고리의 다른 글

JPA in Spring Boot  (0) 2023.11.15
[JPA Core]  (0) 2023.11.15
3 Layer Architecture 역할 분리  (1) 2023.11.09
Database&SQL&JDBC  (0) 2023.11.07
HTTP 데이터를 객체로 처리하는 방법  (0) 2023.11.07