🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [33]일차
🚀33일차에는 스프링에서의 DI (의존성 주입)과 IoC (제어의 역전)를 배우고 이를 활용하여 Bean 생성, @Component 등을 접할 수 있었다.
개념들에 대한 이해는 할 수 있었지만 이를 활용하여 Bean을 생성하고 활용하는 부분에 대해서는 아직 이해가 부족한 것 같아서 회고를 통해 많이 배워야겠다.
스프링 어노테이션(@)
- POJO기반으로써 결합도를 낮추고 유연성을 높인다.
즉, 종속되어 있어 결합도를 높이는 상속을 사용하지 않고도 상속처럼 동작할 수 있도록 어노테이션 사용 - ex. @GetMapping :
@RestController의 자손 격 어노테이션으로 URL 요청이 들어오면 컨트롤러의 클래스를 찾아서 가져와 사용
ex. Get방식 이외로는 PostMapping, DeleteMapping, PatchMapping등이 있다.
Spring Boot 실행클래스
- @SpringBootApplication 어노테이션이 담당
- SpringApplication.run()
➡️스프링 부트(프레임워크)를 실행 - 자바의 클래스를 설정파일로 사용할 경우 “어노테이션”이 중요
➡️어노테이션과 클래스의 내용이 설정파일의 역할을 한다. (Java Config)
(XML형식 설정파일보다 친숙한 자바클래스가 기본 설정파일이 되었다.) - Java Config는 yaml, properties와 함께 설정하는 역할
- SpringBoot 실행클래스가 포함하는 다양한 어노테이션
➡️@Configuration : 해당 클래스가 스프링 설정 클래스임을 나타내는 어노테이션
➡️@EnableAutoConfiguration : 스프링 부트에서 자동구성을 활성화
➡️@ComponentScan : 지정된 패키지와 하위 패키지에서 컴포넌트를 스캔 후 스프링 빈으로 등록
➡️@Component : 스프링에서 컴포넌트를 스캔하고 빈으로 등록할떄 사용되는 가장 기본적인 어노테이션
스프링 코어와 IoC
- DI (의존성 주입), AOP (관련 지향 프로그래밍)이 이에 해당
▶️예제 - DI (Dependency Injection) 의존성 주입
- TV 인터페이스를 구현하는 클래스 STV와 LTV가 있다고 가정
TV tv = new STV();
TV tv = new LTV();
tv.turnOn();
tv.soundDown();
- 추상적인 인터페이스를 구현하여 구체적인 구현체를 만들 수 있을 것이다. (implements)
- ➡️new 생성자를 스프링부트에서 담당하도록 할 수 있다.
// TV 생성 공장 (대신 객체 생성)
public static TV getTV(String tvName){
TV tv = null;
if("STV".equalsIgnoreCase(tvName)){
tv = new STV();
else{
tv = new LTV();
}
return tv;
}
- 이처럼 객체생성을 담당하는 클래스를 만들고 그 클래스가 대신 객체 생성을 해주고 있다.
- 이러한 공장 역할을 스프링부트가 담당하게됨
// main
TV tv = TVFactory.getTV(args[0]);
tv.turnOn();
tv.soundDown();
- args에 STV를 넣어놓으면 main 코드는 바뀌지 않지만 STV가 생성
- 이는 new를 사용하는 방법보다 결합도를 더 낮출 수 있다.
- 즉 객체 생성을 직접하지 않고 스프링에서 담당
스프링은 BeanFactory와 ApplicationContext등의 공장을 통해 이러한 기능을 제공
BeanFactory
- 스프링 프레임워크에서 IoC 컨테이너의 가장 기본적이고 최상위의 인터페이스
- 구현체나 상위 계층의 설정에 의해 동작하는 간단한 인터페이스
- 객체(Bean)의 생성, 의존성 주입 및 생명주기 관리를 담당 (Bean = 일정한 규칙을 가진 자바 객체)
- 특징 :
➡️BeanFactory는 요청 시점에만 Bean을 초기화
➡️단순한 환경에서 사용하며 AOP같은 기술은 사용할 수 없다.
ApplicationContext
- BeanFactory를 확장한 인터페이스로, 대부분의 스프링 프로젝트에서 사용
- @Configuration과 @Bean 같은 설정을 동작시켜줌
어노테이션의 종류
- 객체 생성 어노테이션 (Bean 정의)
➡️@Component, @Service, @Repository, @Controller
➡️스프링 컨테이너에 자동으로 등록 - DI (Dependency Injection, 의존성 주입) 어노테이션
➡️@Autowired, @Qualifier, @Resource
➡️자동으로 Bean의 의존성을 주입 - 설정 어노테이션
➡️@Configuration, @Bean
➡️설정 클래스 정의, Bean 정의를 위해 사용
▶️실습 - Bean 생성
public class MyBean {
private String name;
private int count;
public MyBean() {
}
public MyBean(String name, int count) {
this.name = name;
this.count = count;
}
//... Getter, Setter, toString
}
- 생성할 객체를 정의하고 기본생성자와 매개변수를 받는 생성자를 만듦
public class MyBeanConfig {
@Bean
public MyBean myBean(){
return new MyBean();
}
}
- @Bean
➡️ Bean등록을 의미하는 어노테이션으로
스프링 공장(ex. BeanFactory, ApplicationContext)에게 어떤 빈을 관리할지 알려줌 - @Bean 사용(Java Config방식)은 이전 XML태그의 사용과 달리 오타를 추적할 수 있다.
➡️ ex. <bean id="myBean" class="sample.bean.MyBean"/> (오타를 추적하지 못함) - public MyBean myBean()
➡️리턴타입에 생성할 객체
➡️클래스명은 이 생성할 객체를 가리킬 id를 명시
▶️실습 - 생성된 Bean 사용
package sample.run;
import sample.bean.MyBean;
public class SpringExam01 {
public static void main(String[] args) {
// 1. 직접 객체를 생성하는 경우라면
MyBean bean = new MyBean();
bean.setName("Bruno");
System.out.println(bean.getName());
}
}
- 기존에는 이처럼 new키워드를 통해 인스턴스를 생성하고 Setter메소드를 호출하여 객체를 사용했다.
- ➡️스프링 프레임워크에서는 "스프링 IoC 컨테이너" 에게 객체 생성을 맡긴다.
(스프링이 제공하는 공장 사용 = BeanFactory, ApplicationContext)
스프링의 객체 생성 방법
- XML을 통해 Bean 등록
- Java Config를 통해 Bean등록
- 어노테이션 사용하여 등록 : 가장 간단한 방법으로 내가 만든 객체에게 붙여줄 수 있지만
외부 라이브러리나 외부 코드에게는 붙여주지 못함 - 이외로는 LookUp방식이 있다.
객체 생성 방법 - LookUp방식
// 1. 가져온 후 형변환하는 LookUp방식
MyBean bean1 = (MyBean) context.getBean("myBean");
bean1.setName("Ditt");
System.out.println(bean1);
// 2. 형변환 후 가져오는 LookUp방식
MyBean bean2 = context.getBean("myBean", MyBean.class);
- (MyBean) context.getBean("myBean");
➡️공장에게 getBean()처럼 “myBean”이라는 id의 Bean을 달라는 요청
➡️getBean()하는 시점에 객체가 생성 - 이 방법은 다른 방법들처럼 DI(Dependency Injection)방식이 아닌 DL(Dependency Lookup) 방식
- DL (의존관계 검색) :
의존관계가 있는 객체를 외부에서 주입받는 것이 아닌 의존관계가 필요한 객체에서 직접 검색
- @Bean으로 등록된 Bean은 기본으로 싱글턴 패턴 적용(=@Scope를 적용하지 않음)
- 따라서 bean1과 bean2객체를 생성해도 (== 비교연산자)로 비교해보면
이 둘의 객체의 인스턴스는 “같다”고 판단 (=같은 객체를 여러개 등록할 수도 있음)
id없이 타입만으로 LookUp
// id없이 타입만 가지고 스프링이 매핑(=LookUp)해줌
Book book = context.getBean(Book.class);
System.*out*.println(book);
- 전체 메모리 상에 Book이라는 Bean이 하나밖에 없으면 id없이 타입만 지정해도 객체가 출력됨
(스프링이 Book타입을 찾아냄) - 전체 메모리 상에 Book이라는 Bean이 여러개일때는 id를 지정하지 않으면 (No qualifying bean of type...) 오류 발생
(스프링이 Book타입을 찾을때 Book타입이 여러개이면 오류 발생)
@Scope를 적용한 @Bean
@Bean
@Scope("prototype")
public MyBean myBean2(){
return new MyBean();
}
- @Scope를 명시하여 기본으로 적용되어있던 싱글톤 패턴이 아닌 다른 패턴으로 명시하여 정의 가능
➡️@Scope를 설정하면 객체가 생성되는 시점이 달라짐
- 싱글톤 : 공장이 세워질때 Bean이 만들어짐
- 프로토타입 : Bean이 생성될때마다 같은 id를 사용해도 각기 다른 Bean이 매번 만들어짐
객체 생성 방법 - Java Config방식
// 2-2. Java Config를 통해 Bean 등록
System.out.println("ApplicationContext 생성 전 ---");
ApplicationContext context = new AnnotationConfigApplicationContext(MyBeanConfig.class);
System.out.println("ApplicationContext 생성 후 ---");
- new AnnotationConfigApplicationContext(MyBeanConfig.class)
➡️어떤 빈을 만들 것임을 알려주어야하므로 인자 위치에 MyBeanConfig.class - ApplicationContext : 공장의 인터페이스
- ApplicationConfigApplicationContext : 공장의 구현체
- 또 다른 Bean인 Book객체를 만든 후 테스트해보면
- ApplicationContext 생성 전과 후 사이에 My Bean, Book 객체가 생성된 것 확인 가능
IoC (Inversion of Oontrol)
- 제어의 역전
- 객체 생성과 생명주기 관리의 제어권을 개발자에서 스프링 컨테이너로 넘기는 프로그래밍 패턴 (설계원칙)
- 따라서 DI는 IoC의 한 구현 방법
➡️스프링코어는 IoC컨테이너를 통해 DI를 제공
➡️이처럼 스프링코어는 IoC를 기반으로 작동하기때문에 IoC가 없다면 DI 기능을 구현할 수 없음
▶️실습 - 여러 개의 Bean 사용 (주사위 게임)
public class Dice {
private int face;
public Dice() {
System.out.println("Dice() 실행");
}
public Dice(int face) {
this.face = face;
System.out.println("Dice(int) 실행");
}
public int getNumber(){
return (int)(Math.random() + face) + 1;
}
}
- 이 Dice클래스를 사용할 클래스들로 Player, Game가 있을때 이들은 선언만 하고
초기화되는 시점은 "프로그램이 실행될때 주사위를 주입받게된다"
DI의 주입방법 3가지
- 1.생성자를 통한 주입
- 2.설정자(Setter)를 통한 주입
- 3.필드를 통한 주입 (선언 시 @(어노테이션)을 붙임)
생성자를 통한 주입 예시
// 1. 생성자를 통한 주입
public Player(Dice dice) {
this.dice = dice;
}
- 생성자가 필요하고, 이 생성자는 개발자가 직접 만들어놔야함
설정자를 통한 주입 예시
// 2. 설정자를 통한 주입
public void setDice(Dice dice) {
this.dice = dice;
}
- 스프링 컨테이너가 이 Setter를 통해 주입함
▶️실습 - 1. 생성자 주입의 주사위 게임
public class Game {
private List<Player> list; // 플레이어 리스트 (스프링 공장을 통해서 생성 및 주입 받음)
}
// 1. 생성자를 통한 주입
public Game(List<Player> list) {
System.out.println("Game(List<Player> list) 생성자 실행!");
this.list = list;
}
// 플레이어 리스트를 순회하여 각 플레이어가 play()할 수 있도록 구현
public void play(){
for(Player player : list){
player.play();
}
}
- 플레이어 리스트 또한 스프링 공장을 통해서 객체를 생성하고 플레이어들을 주입받을 것이다.
public class GameConfig {
@Bean
public Dice dice(){
return new Dice(6);
}
}
- Dice 등록
- 설정을 관리하는 GameConfig 클래스에 @Bean을 통해 Dice를 생성하고
- Dice 타입에 생성자의 매개변수로 6(face, 면)을 받아 반환
@Bean
public Player Ben(Dice dice){
Player player = new Player(dice);
player.setName("벤");
return player;
}
@Bean
public Player Fabian(Dice dice){
Player player = new Player(dice);
player.setName("파비안");
return player;
}
- 여러 명의 플레이어를 만든다.
@Bean
public Game game(List<Player> playerList){
return new Game(playerList); // 생성자 주입
}
- 리스트에 들어있는 플레이어들을 스프링 컨테이너가 Game에 자동으로 생성자 주입
- (List<Player> playerList)
➡️스프링이 List에 Player타입을 받아서 playerList를 만들어줌
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(GameConfig.class);
Game game = context.getBean(Game.class); // 하나 밖에 없으므로 id를 지정하지않음
game.play();
}
- 생성자 주입대신 설정자 주입을 통한 방법으로 바꿔볼 수도 있을 것이다.
▶️실습 - 2. 설정자 주입의 주사위 게임
- 설정자 주입을 할때에는 객체에 반드시 기본생성자가 있어야한다.
➡️기본생성자 생성 후 그 기본생성자를 참조하여 Setter메소드에 접근 가능
@Bean
public Player Ben(Dice dice){
// Player player = new Player(dice); // 1. 생성자를 통한 주입
Player player = new Player();
player.setDice(dice); // 2. 설정자를 통한 주입
player.setName("-ㅡ벤ㅡ-");
return player;
}
// ... 다른 플레이어들 @Bean
// 1. 생성자를 통한 주입
// @Bean
// public Game game(List<Player> playerList){
// return new Game(playerList); // 생성자 주입
// }
// 2. 설정자를 통한 주입
@Bean
public Game game(List<Player> playerList){
Game game = new Game();
game.setList(playerList); // 설성자를 통한 주입
return game;
}
- 1. 생성자를 통한 주입방법과 비교해보면
@Bean의 플레이어 객체는 Player 생성자를 부를때 인자를 가지지 않고 기본생성자를 호출한다. - player.setDice(dice);
➡️그 후 player를 참조하여 setDice() Setter메소드를 호출하고 인자로는 dice 주사위 객체를 보낸다. - game.setList(playerList);
➡️생성자를 통해 Game에 주입하는 방법과는 달리 설정자를 통해 Game에 주입하는 방법은 game객체를 참조하여 setList() Setter 메소드에 접근하여 playerList를 인자로 보내준다.
임의 입력값 properties로 관리
- GameConfig 클래스
// 1. 생성자를 통한 주입
@Bean
public Dice dice(){
return new Dice(6);
}
// 2. 설정자를 통한 주입
@Bean
public Dice dice(){
Dice dice = new Dice();
dice.setFace(6);
return dice;
}
- "6"의 값을 인자로 보내서 주사위의 면을 설정하는 부분을 별도의 설정파일로 관리하도록 할 수 있다.
➡️resources → game.properties 생성 → "face=6" 입력
@PropertySource({"classpath:game.properties"})
public class GameConfig { ... }
- @PropertySource({"classpath:game.properties"})
➡️파일명으로 찾지 않고 클래스의 경로로 찾는 방법으로 생성한 설정파일을 가져온다. (game.properties)
// 2. 설정자를 통한 주입
@Bean
public Dice dice(@Value("${face}") int face){
Dice dice = new Dice();
dice.setFace(face);
return dice;
}
- game.properties 설정파일로 지정한 값을 사용할때는
- @Value("${face}") : 키 값을 가져옴
- int face : 가져온 키 값의 value를 int face 에 담음
정리하자면
- Player클래스는 주사위 객체 (Dice)를 스프링 공장을 통해서 주입받음
➡️Dice를 주입받을때는 생성자를 통한 주입 혹은 설정자를 통한 주입이 가능하다. - Game클래스 또한 주사위 객체 (Dice)를 스프링 공장을 통해서 주입받음
- Player클래스에 생성자를 통한 주사위 객체 (Dice)주입 :
Player클래스에 생성자를 만듦
// 1. 생성자를 통한 주입
public Player(Dice dice) {
this.dice = dice;
}
- Player클래스에 설정자를 통한 주사위 객체 (Dice) 주입 :
Player클래스의 Setter메소드 활용
// 2. 설정자를 통한 주입
public Player() { }
public void setDice(Dice dice) {
this.dice = dice;
}
- 설정자를 통해서 주입받을때 Player 클래스는
기본생성자 (public Player())와 Setter 메소드 (setDice(Dice dice)) 를 가져야한다.
- Game클래스에 생성자를 통한 여러플레이어 객체 (List<Player>) 주입
생성자의 매개변수로 List<Player> list를 가져와 주입받음
// 1. 생성자를 통한 주입 (컴포넌트 스캔에 필요)
public Game(List<Player> list) {
System.out.println("Game(List<Player> list) 생성자 실행!");
this.list = list;
}
- Game클래스에 설정자를 통한 여러플레이어 객체 (List<Player>) 주입
Game클래스의 Setter메소드 활용
// 2. 설정자를 통한 주입
public Game(){
System.out.println("설정자를 통한 기본생성자 실행!");
}
public void setList(List<Player> list) {
this.list = list;
}
- 설정자를 통해서 주입받을때 Game 클래스는
기본생성자 (public Game())와 Setter 메소드 (setList(List<Player> list)) 를 가져야한다.
- Game, Player클래스에서 new 키워드를 사용하지 않고 스프링이 대신 객체 생성을 해주고 있는 것
🚀실습 - 3. 필드를 통한 주입 방법 (@Autowired 활용)
- 클래스 내부에 있는 필드에 직접 의존성을 주입
- 스프링 프레임워크에서는 @Autowired를 사용
- 특징
➡️직접 필드에 의존성을 삽입하여 간결한 코드
➡️테스트 및 리팩토링 시에는 의존성을 명시적으로 주입할 방법이 없어 불리할 수 있음
따라서 생성자나 설정자를 사용하지 않아 편리하지만 구조적으로는 명확하지 않다는 것
@Component
public class Dog {
public void coming() {
System.out.println("강아지가 오는 중입니다. ");
}
}
@Component
public class MyDog {
@Autowired // 필드에 의존성 주입
private Dog dog;
public void commanding() { // 강아지에게 명령 수행
dog.coming();
}
}
- @Autowired ➡️ 스프링 컨테이너가 의존성을 자동으로 주입함
- @Component와 함께 사용하여 스프링이 객체들을 자동으로 Bean에 등록하도록 함
- Dog dog = new dog()으로 생성 후 dog.coming()처럼 메소드에 접근하지 않고
필드에 의존성을 주입하는 방법으로 객체를 자동으로 Bean에 등록하여 coming() 이라는 메소드를 수행할 수 있음
자동으로 의존성 주입 - @ComponentScan
- ⭐스프링 컨테이너가 의존성을 주입할 객체에는 컴포넌트임을 명시해야 스캔의 대상이 된다. (@Component)
- 기본적으로 생성자 주입 방식을 사용
@Component
public class Dice {
private int face;
...
}
- 이처럼 @Component로 주사위 객체에 컴포넌트임을 명시한다.
@ComponentScan(basePackages = "sample")
@PropertySource({"classpath:game.properties"})
public class GameConfig {
- 스캔을 담당할 설정 클래스 (Java Config)에 @ComponentScan처럼 명시한다.
- (basePackages = "sample")
➡️반드시 명시
➡️sample패키지 안에 있는 것들만 스캔한다는 설정
@Component
public class Game { ... }
- 이에 따라 Game클래스도 @Component로 등록해야 @ComponentScan역할을 하는 GameConfig 클래스가
자동으로 의존성을 주입할 수 있다.
컴포넌트 등록 시 id 지정
// Book클래스
@Component("spring")
public class Book{ ... }
- 이처럼 지정해주면 "spring"이라는 id로 Book컴포넌트를 얻어올 수 있다.
// main
Book book = context.getBean("spring",Book.class);
- 이처럼 id로 접근 가능
컴포넌트로 취급되는 어노테이션
- @Service : @Component 어노테이션을 포함하므로 "@Service == @Component" 성립
- @Controller : @Component 어노테이션을 포함하므로 "@Controller == @Component" 성립
- @Repository : @Component 어노테이션을 포함하므로 "@Repository == @Component" 성립
- @Configuration : @Component 어노테이션을 포함하므로 "@Configuration == @Component" 성립
⭐@Component가 확장된 형태가 @Service, @Controller, @Repository, @Configuration 등인 것
회원가입 서비스 구조
- repository : 데이터를 담는 기능을 "DAO" 대신 "Repository" 라는 개념을 쓰기도 함
Controller
- Presentation Layer
- 사용자와 커뮤니케이션을 담당 (=보여지는 부분을 담당)
- Request : 사용자의 요청을 받아 Service로 전달
- Response : Service에서 처리된 결과를 다시 사용자에게 응답
- 사용자가 회원가입 요청을 보내면 "해당 데이터를 검증하고 Service에 전달하는 작업"이 Controller의 주된 역할
➡️ex. Spring MVC (사용자 요청을 처리하고 적절한 응답을 제공하기 위한 웹 애플리케이션 프레임워크)
Service
- Business Layer
- 비즈니스 로직을 담당
- 핵심코드가 담기며 Controller와 Repository의 중간 계층
- 회원가입 로직 (입력 데이터 유효성 검증, 중복 사용자 확인, 비밀번호 암호화, 사용자 데이터 Repository에 저장)
➡️ex. 암호화 라이브러리, 외부 API 등을 활용하는 기술 사용 가능
public User registerUser(UserDto userDto) {
// 유효성 검증
if (userRepository.existsByEmail(userDto.getEmail())) {
throw new IllegalArgumentException("이미 사용 중인 이메일입니다.");
}
// ...
}
Repository
- Data Layer
- 데이터를 저장하고, 데이터와 통신하는 부분
즉 데이터베이스와 직접 통신하여 데이터를 CRUD(Create, Read, Update, Delete)하는 것 - 회원가입에서 사용자 정보를 저장하거나 조회하는 쿼리 처리를 담당
➡️ex. JPA (Java Persistence API, 데이터베이스와의 매핑을 간소화하는 ORM 기술) - 데이터베이스 작업을 위한 코드 작성을 최소화하면서 데이터를 다룰 수 있게 함
이처럼 프로젝트 시 업무를 나눠서 진행하는 것이 가장 일반적인 구조
➡️계층형 아키텍처 사용으로 "역할 분담"을 하면
- 각 계층이 명확한 역할을 가져 유지보수가 용이하며
- 기술 스택 변경에도 유연하고
- 각 계층을 개별적으로 테스트할 수 있어 테스트가 용이하다.
🚀 회고를 통해 지나쳤던 용어들에 대해 다시 알아보기도하고 계층형 아키텍처의 예시나 개념도 배울 수 있었다.
필드를 통한 의존성 주입 부분도 개인적으로 공부해볼 수 있었다.
주사위 게임을 통해 의존성 주입을 배워봤는데 단번에 이해하기 어려운 부분이 많았다.
이번 회고로 코드를 한 줄 한 줄 해석해나가며 객체들의 연관성에 대해 흐름을 따라가보니 스프링 IoC컨테이너가 도와주는 작업을 확인할 수 있었다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_35일차_"스프링 AOP" (1) | 2025.01.21 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_34일차_"스프링 Optional, Annotation" (1) | 2025.01.20 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_32일차_"스프링 프레임워크" (0) | 2025.01.16 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_31일차_"리액트 useEffect, Memo프로젝트" (0) | 2025.01.15 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_30일차_"리액트 Express" (0) | 2025.01.14 |