개발놀이터

개발 하다 보면 많이 접하는 스프링 빈 직접 등록하기 : 활용편 본문

Spring/Spring

개발 하다 보면 많이 접하는 스프링 빈 직접 등록하기 : 활용편

마늘냄새폴폴 2024. 5. 15. 17:21

우리는 스프링을 사용하면서 너무 편하게 개발하고 있습니다. 스프링이 정해둔 규칙대로 프로그래밍하면 되기 때문에 정말 쉽게 개발할 수 있죠. 

 

하지만 이게 쉽긴하지만 마냥 쉽진 않습니다. "스프링이 정해둔 규칙" 이라는게 정말 넓고 깊은 지식을 요구하거든요. 

 

일례로 스프링 시큐리티같은 것만 하더라도 "스프링이 정해진 규칙" 이지만 시큐리티를 처음 보는 사람들은 이게 만만치않게 힘들겁니다. 

 

이번에 포스팅할 스프링 빈을 직접 등록하는 것도 마찬가지일 것이라고 생각합니다. 마냥 컴포넌트 스캔을 이용해서 빈을 등록했고 구글링한 예제 코드에서도 @Bean을 이용해서 빈으로 등록하라고는 했지만 어떻게 동작하는 것이고 어떻게 사용하는 것인지 처음엔 알기 쉽지 않거든요. 

 

어떻게 빈을 직접 등록하는 것인지 어떤 상황에서 빈을 직접 등록하는 것인지 그 안에 스프링 개발자들이 숨겨둔 규칙은 무엇인지 한번 본격적으로 알아보도록 하겠습니다. 

 

스프링 빈?

스프링 빈이란 무엇일지 짚고 넘어가도록 하겠습니다. 스프링 빈이란 스프링 컨테이너가 관리하는 자바 객체입니다. 

 

그럼 스프링 컨테이너도 뭔지 설명을 해야겠죠. 스프링 컨테이너도 결국 클래스입니다. 정확히는 인터페이스라고 해야겠지만 아무튼 자바로 만들어진 클래스라는 것이죠. 

 

컨테이너 컨테이너 그래서 뭐가 있나 싶지만 실제로는 그냥 자바로 만들어진 클래스에 불과합니다. 스프링 컨테이너니 IoC 컨테이너니 말만 번지르르하지 사실 그냥 클래스! 라는 것만 명심하시면 더 쉽게 접근할 수 있습니다. 

 

ApplicationContext 라고도 불리는 스프링 컨테이너의 주 역할은 스프링 빈 즉 자바 객체를 관리하는 것입니다. 

 

아니... 그냥 new 로 객체 생성해서 우리가 만들면 되지 뭔 컨테이너인지 뭐시기한테 왜 관리를 맡기냐...라고 생각이 드실 수 있습니다. 

 

외부에서 주입받는 것이 얼마나 큰 장점을 가지는지 제가 설명해보도록 하겠습니다. 

 

IoC (Inversion of Control)

흔히들 제어의 역전이라고 하죠. 이를 설명하기 위해 상황을 가정해보겠습니다. 

 

상황)

기획자가 콜라보레이션을 기획하고 있습니다. A기업과 B기업의 콜라보를 고민하고 있는데 A기업과 B기업에 마진율이 달라서 고객에게 제공하는 할인이 서로 다릅니다. 

 

A기업과 콜라보를 한다면 고객에게 10퍼센트를 할인해줄 수 있고 B기업과 콜라보를 한다면 고객에게 20퍼센트를 할인해줄 수 있습니다. 

 

기획자는 A기업은 비싼만큼 퀄리티가 좋고 B기업은 퀄리티가 조금 떨어지는만큼 싸다는 특징때문에 고민중입니다. 

 

정리)

개발자는 그럼 기획자가 결정하기 전까지 개발을 못하겠네요? 아니요. 둘 다 개발하고 결정되면 그때 바꿔끼워야죠. 수치만 바꿔주는거니까요. 

 

public interface Discount {
	void payment();
}

 

이런 인터페이스를 하나 만들어두고?

 

public class ACompanyPayment implements Discount {
	
    @Override
    public void payment() {
    	// 10퍼센트 할인
        // 할인 정책 엄청 많은 코드와 로직
    }
}

 

public class BCompanyPayment implements Discount {
	
    @Override
    public void payment() {
    	// 20퍼센트 할인
        // 할인 정책 엄청 많은 코드와 로직
    }
}

 

그리고 실질적으로 할인이 이루어지는 코드에선 이렇게 사용합니다. 

 

@Service
public class PaymentService {
	
    private Discount discount = new ACompanyPayment();
    
    public void collaborationPayment() {
    	discount.payment();
    }
}

 

일단 A기업 콜라보로 만들어두고 기획자의 결정을 기다립니다...

 

기획자가 B기업으로 결정했다고 개발자들에게 전했습니다. 만약 이 상황에서 B를 갑자기 만들어야하면 주7일 근무를 해야할지도 모릅니다. 

 

하지만! 우리는 A와 B를 모두 만들어뒀기 때문에 바로 바꿔끼워주기만 하면됩니다.

 

바로 이렇게요.

 

@Service
public class PaymentService {
	
    private Discount discount = new BCompanyPayment();
    
    public void collaborationPayment() {
    	discount.payment();
    }
}

 

자 이렇게 하면 비즈니스 로직인 collaborationPayment의 코드 변경 없이 콜라보를 마무리지을 수 있었습니다...만

 

진짜 코드의 변경이 없었나요? 

 

@Service
public class PaymentService {
	
    private Discount discount = new BCompanyPayment();	// 이 부분이 A에서 B로 변경됨
    
    public void collaborationPayment() {
    	discount.payment();
    }
}

 

일단 OCP에 위배됐고, SRP도 위배됐습니다. (SOLID에 대해서 알고 계시면 이해하기 쉽습니다!)

 

코드의 변경이 있었으니 OCP에 위배되었고 결제를 위해 만들어진 PaymentService가 할인 정책을 결정하는 역할까지 맡고있으니 SRP에도 위배된 것입니다. 

 

이럴 때 "빈"을 직접 등록하면 정말 편해집니다. 

 

public class CollaborationConfig {
	
    @Bean
    public Discount discount() {
    	return new ACompanyPayment();
    }
}

 

@Service
public class PaymentService {
	
    @Autowired
    private Discount discount;
    
    public void collaborationPayment() {
    	discount.payment();
    }
}

 

어떤가요? 자 이제 B기업으로 콜라보를 바꿔보겠습니다. 

 

public class CollaborationConfig {
	
    @Bean
    public Discount discount() {
    	return new BCompanyPayment();
    }
}

 

@Service
public class PaymentService {
	
    @Autowired
    private Discount discount;
    
    public void collaborationPayment() {
    	discount.payment();
    }
}

 

자 PaymentService에서 코드의 변경이 하나도 없었네요. OCP에 위배되지 않았죠? 그리고 SRP도 위배되지 않았네요. CollaborationConfig는 콜라보를 결정하는 책임을 가지고 있고 PaymentService에는 결제하는 책임만을 가지고 있죠. 

 

스프링 컨테이너의 도움을 받아서 우리는 객체지향에 한걸음 가까워졌습니다. 

 

이 내용은 인프런의 김영한 강사님의 강의에도 나와있는 내용입니다. 자세한 내용은 강의를 확인해주세요! 

 

하지만 제가 하고 싶은 말은 이게 아닙니다. 

 

그래서 뭐요?

결국 스프링 컨테이너를 이용하면 SRP, OCP 그리고 심지어 DIP까지 객체지향의 핵심 원칙 세가지를 모두 지킬 수 있다는 것입니다. 

 

하지만 위와 같은 예제는 조금 현실성이 떨어지는 예시입니다. 이런 상황이 있긴 있지만 흔하지 않은 상황이라는 것이죠. 

 

실제로 어떤 상황이 직접 빈을 등록하는 경우인지 예시를 두개 소개해드리려고 합니다. 

 

상황1) 스프링 컨테이너가 뜰 때 객체에 대한 설정을 해줘야할 때

제가 지금 회사에서 개발하고 있는 것이 미디어 서버를 이용한 영상채팅을 구현하고 있는데 이 OpenVidu라는 객체를 @PostContruct 를 이용해서 초기화시켜줍니다. 

 

이게 뭔가 뜻이 있어서 그런건 아니고 오픈소스 예제코드가 이렇게 생겼더라구요...

 

그런데 이 오픈소스를 커스터마이징 하는 과정에서 문제가 발생했습니다. 

 

public class Controller {
	
    private OpenVidu openvidu;
    
    @PostConstruct
    public void init() {
    	openvidu = new Openvidu();
        // 각종 설정들
    }
    
    @GetMapping("/api/sessions")
    public void createSession() {
    	Session session = openvidu.createSession();
        
        // 각종 로직들
    }
}

 

실제 코드를 대충 각색해봤습니다. 

 

어떤 문제였냐하면 이 OpenVidu 객체를 다른 클래스에서도 사용해야 했던 것입니다. 

 

그런데 이 OpenVidu 객체는 Controller 라는 클래스 혼자서만 독식하고있습니다. 또한, 이 OpenVidu라는 객체는 스프링 컨테이너에 의해 관리되는 객체도 아닙니다. 그래서 전역적으로 사용하지 못했던 것이죠. 

 

저는 이 OpenVidu라는 오픈소스를 커스터마이징 하는 과정에서 OpenVidu 객체를 전역적으로 사용해야하기 때문에 이를 스프링 컨테이너의 도움을 받기로 결정했습니다. 

 

public class OpenViduConfig {
	
    @Bean
    public OpenVidu openVidu() {
    	OpenVidu openvidu = new OpenVidu();
        openvidu.set~
        // 각종 설정들
    }
}

 

이렇게 OpenVidu 라는 객체를 스프링 컨테이너에 직접 등록함으로써 테스트를 작성하기도 편해졌고 전역적으로 모든 곳에서 OpenVidu 객체를 사용할 수 있었습니다. 

 

 

상황2) 서버간 통신을 위해 서버를 등록해줘야 하는 상황

서버간 통신을 위해서 어떤 서버에 데이터를 보내야하는지 명시적으로 선언해줘야합니다. 

 

    @PostMapping("/")
    @ResponseBody
    public String paymentPost(HttpServletRequest request) {
        // TODO 회원 인증으로 넘어가야함.
        HttpSession session = request.getSession(false);
        Object value = session.getAttribute("authentication");

        WebClient webClient = WebClient.create("http://localhost:8090");
        
        ResponseEntity<String> responseEntity = webClient.get()
                .uri("/member/authentication/{username}", value)
                .retrieve()
                .toEntity(String.class)
                .block();

        return responseEntity.getBody();
    }

 

WebClient.create() 이 부분이 바로 서버를 선언해주는 곳입니다. 하지만 이런 코드를 한두번 더 써보고 이걸 빈으로 등록해줘야겠다는 생각을 했습니다. 왜냐하면

 

    @PostMapping("/")
    @ResponseBody
    public String paymentPost(HttpServletRequest request) {
        // TODO 회원 인증으로 넘어가야함.
        HttpSession session = request.getSession(false);
        Object value = session.getAttribute("authentication");

        WebClient webClient = WebClient.create("http://localhost:8090");

        ResponseEntity<String> responseEntity = webClient.get()
                .uri("/member/authentication/{username}", value)
                .retrieve()
                .toEntity(String.class)
                .block();

        return responseEntity.getBody();
    }

    @PostMapping("/payment")
    public String payment() {
        WebClient webClient = WebClient.create("http://localhost:8090");

        ResponseEntity<String> responseEntity = webClient.get()
                .uri("/member/payment")
                .retrieve()
                .toEntity(String.class)
                .block();

        return responseEntity.getBody();
    }

 

이렇게 두 곳에서 WebClient를 사용하고 있었는데 코드의 중복 뿐만 아니라 OCP를 위배하기도 합니다. 만약 도메인이 바뀐다거나 포트가 바뀌어버리면 직접 이 컨트롤러 클래스로 와서 하드코딩된 "http://localhost:8090" 이 부분을 직접 수정해줘야합니다. 

 

또한, 만약 기존 8090 서버 말고 8070서버가 추가된다면 어떨까요? 

 

WebClient webClient = WebClient.create("http://localhost:8070");

 

이렇게 계속 추가해줘야합니다. 

 

이럴 때 스프링 컨테이너의 도움을 받아서 전역적으로 관리해주면 정말 편하겠다싶어서 리팩토링을 진행했습니다. 

 

@Configuration
public class ServerConfig {

    private static final String AUTHENTICATION_SERVER = "http://localhost:8090";

    private static final String COST_SETTLEMENT_SERVER = "http://localhost:8070";

    @Bean
    public WebClient authenticationServer() {
        return WebClient.create(AUTHENTICATION_SERVER);
    }

    @Bean
    public WebClient costSettlementServer() {
        return WebClient.create(COST_SETTLEMENT_SERVER);
    }
}

 

이렇게 관리하면 아까 코드가 이렇게 바뀝니다. 

 

    @PostMapping("/")
    @ResponseBody
    public String paymentPost(HttpServletRequest request) {
        // TODO 회원 인증으로 넘어가야함.
        HttpSession session = request.getSession(false);
        Object value = session.getAttribute("authentication");

        ResponseEntity<String> responseEntity = authenticationServer.get()
                .uri("/member/authentication/{username}", value)
                .retrieve()
                .toEntity(String.class)
                .block();

        return responseEntity.getBody();
    }
    
    @PostMapping("/payment")
    public String payment() {
        ResponseEntity<String> responseEntity = authenticationServer.get()
                .uri("/member/payment")
                .retrieve()
                .toEntity(String.class)
                .block();
        
        return responseEntity.getBody();
    }

 

 

이렇게 함으로써 코드의 가독성도 올라가게 되었습니다. 

 

오잉? 코드의 가독성이 어떻게 올라간거지? 

 

기존 코드와 직접 비교하면서 알아보죠. 

 

// 기존 코드
WebClient webClient = WebClient.create("http://localhost:8090");
webClient.get()

// 바뀐 코드
authenticationServer.get();

 

그냥 코드 몇줄 사라진게 코드 가독성이 올라간 것이 아닙니다. 기존엔 localhost:8090이 뭐하는 서버인지 모릅니다. 이걸 알기위해선 어떻게 해야할까요? 

 

맞습니다. 주석을 적어야하죠. 하지만 주석이 있어야만 이해할 수 있는 코드는 잘못된 코드입니다. 주석은 코드의 이해를 도와줄뿐이고 없으면 이해가 안되는 코드는 잘못 만든겁니다. 

 

우리는 localhost:8090 이라는 서버는 인증을 위한 서버라는 것을 직접 명시해주고 있기 때문에 코드를 처음 보는 사람도 이 서버가 인증 서버라는 것을 알 수 있게 되는겁니다. 

 

또한, 서버가 추가되더라도 우리는 추가된 서버를 명시적으로 알 수 있고 애플리케이션 전역적으로 사용할 수도 있습니다. 

 

 

잠깐! 스프링 빈에 대한 틈새 상식!

스프링 빈을 등록하는 과정에서 스프링 내부적인 명명규칙이 있습니다. 컴포넌트 스캔 (@Service, @Repository 등등) 을 사용한다면 클래스이름의 앞글자를 소문자로 바꾼 것을 빈 이름으로 등록합니다. 

 

@Service
public class PaymentService {

}

이렇게 되면 빈 이름은 paymentService

즉 우리가 사용할 때는 

@Autowired
private PaymentService paymentService; 

이렇게 사용하는 것이 일반적인데 이 때 이렇게 사용하는 이유는 paymentService가 빈 이름이기 때문

우리가 빈을 직접 등록할 때
@Bean
public WebClient authenticationServer {

}

이렇게 만들면 빈이름은 authenticationServer가 됩니다.

물론 이렇게 정해줄 수도 있습니다.
@Bean(name = "authenticationServer")
public WebClient authenticationServer() {
    return WebClient.create(AUTHENTICATION_SERVER);
}

 

 

그래서 결론은!

제가 예시로 들었던 콜라보는 이해를 돕기 위한 것이지 실제 많이 일어나는 상황은 아닙니다. 하지만 스프링 빈을 직접 등록해야하는 경우는 정말 흔하게 일어납니다. 

 

스프링 빈은 스프링 애플리케이션 전체적으로 사용할 수 있는 "슈퍼전역변수" 같은 것이라고 생각하시면 편합니다. 

 

우리는 변수를 배울 때 지역변수 전역변수를 배우곤 합니다. 지역변수는 메서드 안에서만 사용가능한 변수, 전역 변수는 클래스 내부에서만 사용가능한 변수라고말이죠. 하지만 애플리케이션 전반적으로 모두 사용해야하는 변수는 어떤가요? 전역변수로 커버할 수 없습니다. 

 

이렇듯 모든 클래스에서 사용할 수 있는 변수를 "슈퍼전역변수" 라고 한다면 그것은 스프링 빈이 될 것입니다. 

 

조금 어려운 내용이지만 static 키워드를 이용하면 어느정도 흉내낼 수 있습니다. 하지만 static은 GC의 영향 밖에 있기 때문에 객체가 많아지면 메모리를 많이 잡아먹어서 서버의 성능에 큰 영향을 미칠 수 있습니다. 

 

 

이번 포스팅을 정리하면 다음과 같습니다. 

 

  1. 뭔가 같은 행동을 반복하고 있을 때 = 중복이 일어날 때
  2. 내 프로젝트 전반적으로 모든 곳에서 사용해야하는 변수가 있는 경우

 

마치며

이번엔 긴 글을 적게 되었습니다. 이번 포스팅을 통해 저도 어느때에 직접 빈을 등록하는지에 대해서 정리하는 계기가 되었습니다. 

 

스프링 빈은 스프링을 사용하는 개발자에게 기본 중에 기본이지만 저도 이 기본기를 잘 활용하고 있냐하면 그건 아닌 것 같은 느낌이 듭니다. 저도 언제쯤 훌륭한 개발자들과 어깨를 나란히할지 모르겠습니다 ㅎㅎ..

 

저도 포스팅을 계속 쓰면서 정진하도록 하겠습니다. 여러분들 모두 화이팅입니다!