본문 바로가기
공부/springboot

46일차 복습

by 샤샤샤샤 2023. 1. 19.
public class Member {
    private String name;

    public Member(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //싱글톤 만들기
    private static Member m = null;
    public static Member getInstance() {
        if( m == null ){
            m = new Member("홍길동");
        }
        return m;
    }
}

 

먼저 들어가기에 앞서, 설명을 위한 Member클래스부터 만들자.

name필드를 포함한 생성자 함수,

get/set함수,

싱글톤 패턴이 들어간 클래스이며, 필드의 값은 null이다.

 

DI(Dependency Injection) 의존 주입

주입에는 직접 주입과 의존 주입이 존재한다. 여기서 주입이란 뭘까? 영어를 직역한 것이여서 다소 어색하지만, 풀어서 설명하자면 함수 내에서 클래스 객체(인스턴스)를 사용하는 것을 의미한다. 따라서 직접 주입은 평소 일반적으로 코드를 짤때 new를 사용해 객체를 선언하는 방식을 말한다.

Member m = new Member("대통령");

이와 같은 방식으로 객체를 만들어 사용하는 것이 직접 주입이다.

 

반대로 의존주입은 함수 밖으로부터 인스턴스를 공급 받는다.

public class Ex03DiApplication {

	public static void main(String[] args) {

		Member m = new Member("임꺽정"); // 직접 주입
		System.out.println( m.getName() );

		printNameUseDI( null ); // null값 입력
		printNameUseDI( new Member("이소룡")); // 객체 입력
		printNameUseDI( Member.getInstance() ); // 싱글톤 객체 입력
	}
	public static void printNameUseDI(Member m){
		if( m == null ){
			System.out.println("m == null입니다.");
			return;
		}
		System.out.println( m.getName() );
	}
}//class

결과는 다음과 같다.

이를통해 몇가지 사실을 알 수 있다.

1.  의존 주입(DI)는 null safety를 보장한다. 즉, null값으로 인해 null point exception(NPE)가 발생하지 않고 프로그램이 돌아간다.

2. 싱글톤 패턴의 객체도 받을수 있다.

 

직접 주입 대신 의존주입을 사용하는 이유는 아래와 같다.

직접 주입

문제점 :

1.싱글톤이 기본적으로 없다.

2.의존성(코드결합)이 강해서 수정,삭제시 오류발생 확률이 존재한다.

3.NULL Exception이 발생할 확률이 높다.

4.싱글톤 안의 데이타 유지가 불편하다.

 

의존 주입

특징 :

1.자바객체를 스프링에서 관리(빈(Bean))하고, 기본적으로 싱글톤 패턴으로 만들어 관리한다.

2.의존성을 느슨하게 해서 수정,삭제시 오류를 줄여준다.

3.NULL Exception이 발생활 확률을 줄여준다.

4.싱글톤 안의 데이타 유지가 편하다.

 

Bean으로 객체 관리하기

Bean은 스프링 컨테이너가 관리하는 자바 객체를 말한다. 기존 자바 코드에서 객체는 사용자가 제어해야 하는 것이었으나, 스프링에서는 사용자가 new를 통해 직접 생성하지도 않고, 제어 역시 Bean으로 만들어 스프링이 관리한다.

Bean으로 등록되면, 따로 설정을 주지 않는 이상 무조건 싱글톤이 된다.

메서드를 Bean으로 등록시키면 인스턴스처럼 사용할수 있다.

 

크게 두가지 방법이 존재한다.

1. @Configuration 과 @Bean 어노테이션을 통해 빈으로 등록하기

거의 쓰이지 않으며, 개발자가 컨트롤 불가능한 외부의 라이브러리들을 Bean으로 등록하고 싶을때 사용된다.

@Configuration은 스프링 설정 클래스라는 것을 의미하며, 이 클래스 내부에서 스프링에 대한 설정이 가능하다. 그 안에서 함수 위에 @Bean을 쓰면 해당 메서드가 빈으로 등록된다.

 

package com.study.springboot;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//@Configuration + @Bean은 쌍으로 사용됨.
//@Configuration : 스프링 설정 클래스이라는 뜻
@Configuration
public class AppConfig {
    //@Bean : 메서드위에 기술하여 반환되는 객체를 스프링 빈으로 등록해준다.
    @Bean
    public Member member(){
        System.out.println("member객체가 빈으로 등록됨.");
        return new Member("머스크");
    }
    //보안관련, 파일업로드 - 설정 메소드 등이 들어간다...
}
package com.study.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


@SpringBootApplication
public class Ex04SpringDiApplication {

	public static void main(String[] args) {
		//ApplicationContext : 스프링 컨텍스트 또는 스프링 풀(Pool)이라고도 함.
		//                   : 자바 빈을 관리하는 클래스
		ApplicationContext context =
				new AnnotationConfigApplicationContext(AppConfig.class);
		//빈의 이름은 클래스의 이름으로 자동 저장(첫글자는 소문자로 변환됨)
		Member member = (Member)context.getBean("member");
		System.out.println( "Application Context에서 찾은 빈:" + member.getName() );

		Member member2 = (Member)context.getBean("member");
		System.out.println( member ); //31f9b85e
		System.out.println( member2 ); //31f9b85e -- 주소값이 같다. 싱글톤.

		SpringApplication.run(Ex04SpringDiApplication.class, args);
	}

}

ApplicationContext 객체명 =

     new AnnotationConfigApplicationContext(AppConfig.class); : AppConfig 자바 파일 내에 있는 모든 설정 클래스를 인식한뒤, 그 내부의 어노테이션에 따라 빈을 골라내 ApplicationContext 객체에게 관리를 맡긴다.

ApplicationContext : 스프링 컨텍스트. 빈을 관리하는 클래스

getBean() : 빈을 가져온다.

 

2. @Component + @Autowired

기본 서버 구동파일에 존재하는 @SpringBootApplication 어노테이션은 기본적인 스프링부트 앱 개발환경과 설정을 해주는 어노테이션 3개를 합쳐둔 어노테이션이다.

각 어노테이션들의 역할은 아래와 같다.

   1) @ComponentScan : @Component가 붙은 클래스를 Bean으로 등록한다.

   2) @EnableAutoConfiguration : 스프링 프레임워크의 기본적인 기능을 활성화할때 사용하는 어노테이션이다.

   3) @SpringBootConfiguration : @Configuration이 붙은 클래스를 스프링 프레임워크의 설정 클래스로 등록한다. 설정을 클래스(자바코드)로 가져온다.

이중 우리가 살펴볼 것은 1번이다.

 

@Component를 사용하면 세가지 방식으로 빈을 주입받을수 있다.

    1) 필드(맴버변수) 주입 -- 가장 일반적인 형태

    2) 수정자(Setter) 주입

    3) 생성자 (Constructor) 주입 -- 가장 권장되는 방식

 

이를 설명하기 위해 위에서 만들었던 Member클래스를 조금 수정해보자.

package com.study.springboot;

import org.springframework.stereotype.Component;

// @Component : @Component가 붙은 클래스를 자바 빈으로 등록한다.
@Component
public class Member {
    private String name = "사임당";
    //기본생성자(필드가 없는 생성자)
    public Member() {
    }
    //필드가 있는 생성자
    public Member(String name) {
        this.name = name;
    }
    //getter
    public String getName() {
        return name;
    }
    //setter
    public void setName(String name) {
        this.name = name;
    }
}

1.필드주입

@Controller
public class MainController {
    @GetMapping("/")
    @ResponseBody
    public String main() {
        return "스프링 웹 애플리케이션~";
    }
    @Autowired
    private Member member1; //스프링 역할 : Member member = new Member();
    @Autowired  //@Autowired - 매 필드마다 기술해야 됨
    private Member member2;

    // 요청URL : localhost:8080/field
    @GetMapping("/field")
    @ResponseBody
    public String field(){
        System.out.println( member1.getName() );
        return member1.getName();
    }
 };

@Autowired를 작성하면 알아서 객체를 만들어준다. 이때, 스프링은 만들고자 하는 클래스의 객체를 판별후, Bean에 해당 객체가 등록되어 있다면 Bean을 이용해 객체를 생성한다. 그러나 Bean으로 등록되지 않은 클래스라면 오류가 발생하며 서버가 멈춘다.

@Autowired : Bean으로 관리되고 있는 클래스의 객체를 생성한다.

 

결과는 아래와 같다.

 

2. 수정자 주입

package academy.Academy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

// @Controller : HTTP 요청을 처리하는 클래스에 지정한다.
// @Controller 내부에 @Component 어노테이션이 들어가 있음.
@Controller
public class MainController {
    // 요청URL : localhost:8080/
    @GetMapping("/")
    @ResponseBody
    public String main() {
        return "스프링 웹 애플리케이션~";
    }
    //1. 필드 주입
    @Autowired
    private Member member1; //스프링 역할 : Member member = new Member();
    @Autowired  //@Autowired - 매 필드마다 기술해야 됨!!
    private Member member2;

    // 요청URL : localhost:8080/field
    @GetMapping("/field")
    @ResponseBody
    public String field(){
        System.out.println( member1.getName() );
        return member1.getName();
    }

    //수정자 주입
    private Member member3;
    @Autowired
    public void setMember(Member member){
        //스프링 역할 : this.member3 = new Member();
        this.member3 = member; // @Autowired덕분에 new생략
        member3.setName("임꺽정");
    }

    // 요청URL : localhost:8080/setter
    @GetMapping("/setter")
    @ResponseBody
    public String setter(){
        System.out.println( member3.getName() );
        return member3.getName();
    }
}

3. 생성자 주입

생성자 주입은 final을 사용할 수 있으며, 생성자 함수로 객체가 생성되어 주입된다. null safety를 제공받는다.

package academy.Academy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

// @Controller : HTTP 요청을 처리하는 클래스에 지정한다.
// @Controller 내부에 @Component 어노테이션이 들어가 있음.
@Controller
public class MainController {
    // 요청URL : localhost:8080/
    @GetMapping("/")
    @ResponseBody
    public String main() {
        return "스프링 웹 애플리케이션~";
    }
    //1. 필드 주입
    @Autowired
    private Member member1; //스프링 역할 : Member member = new Member();
    @Autowired  //@Autowired - 매 필드마다 기술해야 됨!!
    private Member member2;

    // 요청URL : localhost:8080/field
    @GetMapping("/field")
    @ResponseBody
    public String field(){
        System.out.println( member1.getName() );
        return member1.getName();
    }

    //수정자 주입
    private Member member3;
    @Autowired
    public void setMember(Member member){
        //스프링 역할 : this.member3 = new Member();
        this.member3 = member; // @Autowired덕분에 new생략
        member3.setName("임꺽정");
    }

    // 요청URL : localhost:8080/setter
    @GetMapping("/setter")
    @ResponseBody
    public String setter(){
        System.out.println( member3.getName() );
        return member3.getName();
    }

    //3. 생성자 주입 - final 사용 가능(객체 재할당 방지)
    //             - 생성자로서 미리 주입을 받는다. null safety를 제공받는다.
    private final Member member4;
    @Autowired
    public MainController(Member member){
        this.member4 = member;
        member4.setName("전우치");
        System.out.println(member4.getName());
    }
    // 요청URL : localhost:8080/constructor
    @GetMapping("/constructor")
    @ResponseBody
    public String constructor() {
        System.out.println(member4.getName());
        return "constructor호출됨   " + member4.getName();
    }
}

보다시피 member4의 값이 전우치가 아닌 임꺽정인 것을 볼수 있다. 그렇다고 바뀌지 않았던 것 역시 아니다.

콘솔 창을 보면 바뀌었던 것은 확실한데 왜 다시 임꺽정으로 돌아간걸까?

해답은 생성자 함수에서 값을 바꿧다는데 있다.

Bean으로 만들어지는 객체는 싱글톤이기에, 생성자 함수는 객체가 선언될때마다 실행된다. 따라서 객체가 선언될때 "전우치"가 name필드의 값으로 할당되지만, 이후 다시 수정자 주입 코드에서 "임꺽정"으로 값을 재할당했고, 그 이후에 객체를 다시 선언한 적이 없기 때문에 "임꺽정"으로 유지된 것이다.

member4가 final이 선언되었으니 값이 변하면 안되는 것 아니냐고 의문을 품을수도 있다. 그러나 final은 member4에게 적용되는 속성이다. 따라서 member4 = null; 는 오류가 발생할것이나, member4의 내부 필드의 값을 변경하는 것은 가능하다.

 

추상화 객체를 의존 주입 하기

추상화 객체, 즉, 인터페이스의 객체나 추상화 클래스의 객체를 어떻게 만들수 있을까? 결론만 말하자면 불가능하다. 엄밀히 말하자면 추상화 객체를 주입한다기 보다는, 추상화 객체를 구체화시킨 구현체를 주입하고, 그렇게 만들어진 객체 type을 상속한 추상 클래스/ 인터페이스 타입으로 주는 것이다.

 

코드를 보며 살펴보자. 먼저 인터페이스와 구현체를 만들자.

package com.study.springboot;

public interface ICard {
    //가상함수(추상화 메소드)만 들어감
    public void buy(String itemName);
}
package com.study.springboot;

import org.springframework.stereotype.Component;

//@Component : 클래스를 객체로 만들어주는 어노테이션이다.
// Bean의 이름은 "cardA"이다.
@Component("cardA") // Bean이 될 이름을 지정하는 방법
public class CardA implements ICard {
    @Override
    public void buy(String itemName) {
        System.out.println("CardA로 "+itemName+"을 샀다.");
    }
}
package com.study.springboot;

import org.springframework.stereotype.Component;

@Component("cardB")
public class CardB implements ICard {
    @Override
    public void buy(String itemName) {
        System.out.println("CardB로 "+itemName+"을 샀다.");
    }
}

Member클래스 역시 추상화된 ICard 를 주입받을수 있도록 수정해보자.

package com.study.springboot;

import org.springframework.stereotype.Component;

@Component("member")
public class Member {
    private String name;
    private ICard iCard;

    public Member() {
    }

    public Member(String name, ICard iCard) { // ICard 타입의 매개변수를 받는 생성자 함수
        this.name = name;
        this.iCard = iCard;
    }

    public String getName() { // name getter
        return name;
    }

    public void setName(String name) { // name setter
        this.name = name;
    }

    public ICard getiCard() { // iCard getter
        return iCard;
    }

    public void setiCard(ICard iCard) { // iCard setter
        this.iCard = iCard;
    }
}

이제 이렇게 만들어진 인터페이스 ICard 타입의 객체를 만들어서 member에 Setter(수정자)로 주입해보자.

package com.study.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MainController {
    //요청URL : localhost:8080/
    @GetMapping("/")
    @ResponseBody
    public String main(){
        return "스프링부트 앱입니다.";
    }
    
    //member 필드주입 받는다.
    @Autowired
    private Member member1;

    @GetMapping("/member")
    @ResponseBody
    public String member(){
        member1.setName("홍길동");
        return "member() 호출됨.";
    }
    
    //---------------------iCard 객체 필드 주입--------------------------
    
    @Autowired
    @Qualifier("cardB") //cardB 객체를 주입하도록 기술함.
    ICard iCard; //CardA 또는 CardB 클래스 객체를 구현체로 주입받는다.

    @GetMapping("/card")
    @ResponseBody
    public String card(){
        member1.setiCard( iCard );
        member1.getiCard().buy("콩나물");
        return "card() 호출됨";
    }
}

 

필드 주입 방식으로 member1객체가 의존 주입했고, 이어서 set함수를 통해 name필드를 지정해주었다.

그리고 @Autowired어노테이션으로 Bean으로 관리되고 있는 ICard 객체를 불러와 객체를 생성했다. 엄밀히 말해 ICard는 Bean으로 관리되고 있지 않다는 점을 주의하자. ICard를 구현한 CardA와 CardB 클래스의 객체가 Bean으로 관리되고 있고, 다형성을 통해 CardA의 타입이 ICard가 될 수 있을 뿐이다.

그렇다면 이때 어떤 객체를 가져와야 할까? cardA일까, cardB일까?

이를 분명하게 지정해주는 어노테이션이 @Qalifier( '빈 이름' ) 이다. 만약 이 어노테이션이 없다면, ICard 인터페이스는 근본적으로 객체를 생성할 수 없기에 오류가 발생할 것이다.

 

인터페이스를 상속받은 구현체의 객체를, 다형성을 이용해 인터페이스 타입을 부여하는 것이라는 것일 기억하자.

 

@Qalifier() : 빈을 이름으로 불러오는 어노테이션

 

Lombok디펜던시

Lombok은 귀찮게 일일이 작성해야 하는 코드들을, 간단한 어노테이션으로 줄여주는 기능을 지닌 디펜던시다. 코드를 컴퓨터가 이해할수 있는 언어로 바꾸는 과정(컴파일링)에서 특정 어노테이션이 나오면 미리 정해진 코드를 추가하도록 한다고 생각하면 된다.

어노테이션들은 다음과 같다.

@Getter : getter 자동생성
@Setter : setter 자동생성
@NoArgsConstructor : 매개변수 없는 기본생성자 자동생성
@AllArgsConstructor : 모든 필드를 매개변수로 받는 생성자 자동생성
@RequiredArgsConstructor : final이나 @NonNull인 필드만 매개변수로 받는 생성자 자동생성
			   			   동시에 자동적으로 Bean으로 객체 생성.
@NonNull : null을 허용하지 않는 객체 Bean 자동생성
@Nullable : null을 허용하는 객체 Bean 자동생성
@Data : @Getter, @Setter,@RequiredArgsConstructor, @ToString, @EqualsAndHashCode을
		한꺼번에 설정해주는 어노테이션
@ToString : toString 메소드 자동생성
@EqualsAndHashCode : equals, hashCode 메서드 생성

Lombok를 사용했다면, 한번쯤은 구조창을 켜서 옳바르게 생성되었는지 확인해보자.

인텔리제이의 경우 단축키는 Alt + 7 이며, 보기 - 도구창 - 구조 를 눌러 확인해볼수도 있다.

 

인텔리제이와 스프링팀에서는 @Autowired를 통한 필드 주입보다 @RequiredArgsConstructor을 이용한 생성자 주입을 권장한다.

예제를 보자.

package com.study.springboot;

import lombok.*;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class Member {
    private final String username="홍길동"; // 여기서 값을 왜 줬을까?
    // private final String username; // 초기화 하지 않고 선언만 해두면 오류가 발생한다.
    //@NonNull     마찬가지로 NonNull 속성을 부여하면 오류가 발생한다. 설명은 아래에서 하겠다.
    private String password;
}
package com.study.springboot;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class MainController {
    @GetMapping("/")
    @ResponseBody
    public String main() {
        return "Lombok 예제입니다.";
    }
    // @RequiredArgsConstructor을 통해 자동적으로 Member클래스의 빈이 존재하는지 확인하고 
    //  존재한다면 final이 선언된 객체를 만든다.
    private final Member member;
    
 // ---------위 코드를 사용하면 아래 코드가 생략될수 있다.------
    //생성자 주입
//    @Autowired
//    public MainController(Member member){
//        this.member = member;
//        System.out.println("생성자 주입됨:"+member);
//    }
//
}

위와 같은 생성자 함수를 사용하면 직접 코드를 짜는데 수고를 줄일수 있고, 코드 줄수를 줄이는 효과를 볼 수 있다.

헌데 나는 이 부분에서 한참동안 해맸었다.

 

처음 내가 짯던 코드는 아래와 같았다.

package com.study.springboot;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class Member {
    private final String username;
}

그런데 다음과 같은 오류가 발생하며 자꾸 서버가 멈췄다.

Parameter 0 of constructor in com.study.springboot.Member
required a bean of type 'java.lang.String' that could not be found.

직역하면 "이 클래스에 있는 생성자의 매개 변수는 "String" 타입의 빈이 필요한데 찾을수 없다 "는 것이다.

이 문제가지고 속을 오래 썩였다. 도무지 이해가지 않던 것이, Member 클래스의 final 필드 변수 username에 값을 할당하면 오류가 발생하지 않는다는 것이었다. 선언만 하면 오류가 생기는데, 초기화하면 오류가 생기지 않는다는 것이 이해가 되지 않아서 이것저것 시도해보던 도중, 한가지 사실을 깨달았다.

    // Member의 필드 변수
	private final String username;
    // MainController에서 만든 생성자 주입 코드
	private final Member member;

 

둘의 코드 형식이 똑같다는 것이다.

 

@RequiredArgsConstructor는 기본적으로 생성자 주입을 위한 어노테이션이다. 쉽게 설명하자면, @Autowired의 기능이 내장되어 있어, final이 붙은 변수를 객체 선언으로 자동적으로 인식해, 변수의 타입과 동일한 클래스의 빈이 존재하는지 확인하고, 빈이 존재한다면 객체를 생성하는 기능이 있다.

이 경우는, "String"라는 클래스의 username 이라는 이름을 가진 객체를 만들어라, 라고 자동적으로 인식하고 bean목록을 뒤져보고는, "String" 클래스 빈은 없는데? 하면서 오류를 일으킨 것이다.

이로서 @RequiredArgsConstructor는 반드시 생성자 주입할때만 사용해야 하며, 변수의 생성자 함수 선언에는 사용하면 안된다는 것을 깨달았다.

 

 

'공부 > springboot' 카테고리의 다른 글

MVC -- Model(2) : 데이터 받아오기  (0) 2023.01.25
데이터의 전송 타입( Get/ Post)  (0) 2023.01.25
MVC -- Model(1): 데이터 전송하기  (0) 2023.01.25
타임리프 레이아웃  (0) 2023.01.25
45일차 복습  (0) 2023.01.17