특정 스프링 빈 조회

스프링 컨테이너에서 이름이나 타입으로 특정 빈을 조회하는 방법이다.

ApplicationContextBasicFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AppConfig;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextBasicFindTest {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("Bean 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //System.out.println("memberService = " + memberService);
        //System.out.println("memberService.getClass() = " + memberService.getClass());
    }

    @Test
    @DisplayName("Bean 이름 없이 Type으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 Type으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean(MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("Bean 이름으로 조회 실패")
    void findBeanByNameFailed() {
        //ac.getBean("hello", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("hello", MemberService.class));
    }
}

 

앞서 사용했던 getBean() 메소드에 스프링 빈 이름과 타입을 명시하면 특정 빈을 조회할 수 있다. 빈 이름은 중복되면 안 되니 이름으로만 검색해도 원하는 빈을 조회할 수 있지만, 타입을 명시함으로 조금 더 정확한 결과를 얻을 수 있다. 또한 타입에 .class를 붙이는 이유는 스프링이 타입을 명확하게 알 수 있어 타입에 맞는 빈을 반환하고, 개발자는 별도의 캐스팅 없이 사용할 수 있기 때문이다.

스프링 빈 조회 시 동일한 타입이 둘 이상일 때

ApplicationContextSameBeenFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.member.MemberRepository;
import spring.basic.member.MemoryMemberRepository;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeenFindTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("같은 Type이 둘 이상 있으면 중복 오류 발생")
    void findBeanByTypeDuplicate() {
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("같은 Type이 둘 이상 있을땐 Bean 이름을 지정")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("같은 Type을 모두 조회")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }


        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

우선 기존에 사용하던 AppConfig에는 중복된 타입이 없어서 테스트 코드에 @Configuration 어노테이션을 단 static 클래스로 설정 역할을 할 SameBeanConfig를 작성하고, ApplicationContext 생성 시 인자로 넘긴다.

 

해당 테스트 코드를 실행시키면 스프링 빈 팩토리에는 memberRepository1과 memberRepository2, 두 개의 빈이 저장된다. 이 상태에서 getBean() 메소드로 MemberRepository.class 타입의 빈을 조회하면 NoUniqueBeanDefinitionException이 발생하게 된다. 즉, 조회 결과가 유니크하지 않아서 발생하는 오류다.

 

같은 타입의 빈을 조회하려면 getBeansOfType() 메소드를 사용해서 조회하며, 결과는 Map<String(빈 이름), Type> 형태로 반환된다. 해당 테스트 실행 결과는 아래와 같다.

key = memberRepository1 value = spring.basic.member.MemoryMemberRepository@4879dfad
key = memberRepository2 value = spring.basic.member.MemoryMemberRepository@4758820d
beansOfType = {memberRepository1=spring.basic.member.MemoryMemberRepository@4879dfad, memberRepository2=spring.basic.member.MemoryMemberRepository@4758820d}

스프링 빈 상속 관계 조회

ApplicationContextExtendsFindTest
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.discount.DiscountPolicy;
import spring.basic.discount.FixDiscountPolicy;
import spring.basic.discount.RateDiscountPolicy;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextExtendsFindTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 Type으로 조회 시 자식이 둘 이상 있으면 Bean 이름을 지정한다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 Type으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 Type으로 모두 조회")
    void findAlllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 Type으로 모두 조회하기(Object)")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

 

기본적으로 부모 타입으로 조회하면 자식 타입도 함께 조회된다. 그래서 모든 자바 객체의 부모인 Object 타입으로 조회하면 모든 스프링 빈이 조회된다. 이번에도 TestConfig를 작성하여 인자로 넘겼기 때문에, getBean() 메소드로 DiscountPolicy.class 타입 조회 시NoUniqueBeanDefinitionException이 발생하게 된다. 따라서 특정 자식 타입으로 조회하거나, getBeansOfType() 메소드로 조회하면 된다.

BeanFactory와 ApplicationContext

그림과 같이 BeanFactory는 스프링 컨테이너의 최상위 인터페이스이며, ApplicationContext는 BeanFactory를 상속한 인터페이스이다. AnnotationConfigApplicationContext 등은 ApplicationContext를 상속한 구현체이다. 각각의 역할과 특징은 아래와 같다.

  1. BeanFactory의 역할과 특징
  • 스프링 컨테이너의 최상위 인터페이스
  • 빈의 생성, 조회, 의존성 주입 등 기본적인 기능 제공
    • 위에서 사용한 getBean 메소드 제공
  1. ApplicationContext의 역할과 특징
  • BeanFactory를 상속한 인터페이스
  • BeanFactory의 모든 기능을 포함하며 애플리케이션에 필요한 다양한 부가 기능 제공
    • 메시지 소스(MessageSource) : 국제화 지원
    • 이벤트 발행(ApplicationEventPublisher) : 이벤트 기반 프로그래밍 지원
    • 리소스 로딩(ResourcePatternResolver) : 다양한 경로의 리소스 로딩
    • 환경 변수 관리(EnvironmentCapable)
    • 계층적 빈 팩토리(HierarchicalBeanFactory)
    • 어노테이션 기반 DI 지원, 모든 빈 스코프 지원

XML 기반의 설정

앞서 언급했듯 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정을 읽을 수 있도록 유연하게 개발되었고, 지금까지는 대부분 어노테이션 기반의 AnnotationConfigApplicationContext를 사용해 Java로 설정 파일을 만들었다. 최근에는 XML 기반의 설정을 잘 사용하지 않지만 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있다. GenericXmlApplicationContext를 사용한다.

appConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="spring.basic.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="spring.basic.member.MemoryMemberRepository" />

    <bean id="orderService" class="spring.basic.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>

    <bean id="discountPolicy" class="spring.basic.discount.RateDiscountPolicy" />
</beans>
XmlAppContext
package spring.basic.xml;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import spring.basic.member.MemberService;

import static org.assertj.core.api.Assertions.assertThat;

public class XmlAppContext {
    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

테스트 코드를 보면 알 수 있듯이 appConfig.xml과 계속 사용하던 AppConfig.class는 거의 유사하다.

스프링 빈 설정 메타 정보(BeanDefinition)

스프링 컨테이너가 다양한 형식의 설정을 읽을 수 있는 이유는 BeanDefinition이라는 빈의 생성, 설정, 관리에 필요한 메타정보를 담아두는 추상화가 있기 때문이다. 스프링 컨테이너는 Java, XML, Groovy 등 다양한 형식의 설정 파일을 읽어 BeanDefinition 객체로 변환한 뒤, 이 메타 정보를 기반으로 실제 빈 객체를 생성하고 관리한다. 각 @Bean 또는 태그마다 하나의 BeanDefinition이 생성된다.

 

방금 말한 것과 같이 BeanDefinition의 동작 흐름은 1. 개발자가 Java, XML, Groovy 등 설정 파일을 통해 빈을 정의 → 2. 스프링 컨테이너가 설정 정보를 읽어(각 형식에 맞는 Reader가 이 역할을 한다) BeanDefinition 객체로 변환 → 3. BeanDefinition에 담긴 메타 정보를 참고해 빈을 생성, 의존성 주입, 라이프사이클 관리 등을 수행하게 된다.

 

BeanDefinition이 담고 있는 주요 정보는 아래와 같다.

  • beanClassName : 생성할 빈의 클래스 명
  • factoryBeanName : 빈을 생성하는 팩토리 역할의 빈 이름
  • factoryMethodName : 빈을 생성하는 팩토리 메소드 이름
  • scope : 빈의 스코프
  • lazyInit : 지연 초기화 여부(빈을 실제 사용할 때까지 생성 지연)
  • initMethodName : 빈 초기화 메소드 이름
  • destroyMethodName : 빈 소멸 전 호출할 메소드 이름
  • constructor arguments, properties : 빈 생성 시 주입할 생성자 인자와 프로퍼티 값
  • autowireCandidate, primary : 자동 주입 대상 여부, 우선순위

하지만 실무에서 BeanDefinition을 직접 다룰 일은 거의 없고 자동으로 생성, 관리되는대로 사용한다고 한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 7  (0) 2025.04.29
Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30

스프링 컨테이너 생성

스프링 컨테이너 생성은 이전에 작성한 MemberApp 또는 OrderApp에서 사용한 방법으로 생성한다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext를 보통 스프링 컨테이너라고 부른다. ApplicationContext는 인터페이스인데, AnnotationConfiApplicationContext라는 ApplicationContext의 구현체를 사용하여 생성한다. AnnotaionConfig가 붙은 이름처럼 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 생성하게되며, 이외에 XML이나 Groovy를 통한 생성 방법도 있다.

사진처럼 ApplicationContext 인터페이스로 구현한 구현체가 AnnotaionConfigApplicationContext를 포함해서 많다.

스프링 컨테이너의 생성 과정

  1. 어노테이션 기반의 AppConfig.class를 바탕으로 스프링 컨테이너가 생성된다.
    • 스프링 빈 저장소도 같이 생성된다.
    • 스프링 빈 저장소는 빈 이름과 빈 객체의 쌍으로 구성된다.
  2. AppConfig.class에 @Bean 어노테이션을 가진 모든 메소드를 스프링 빈 저장소에 저장한다.
    • 빈 이름은 기본적으로 메소드 이름을 사용하지만, 어노테이션에 name으로 직접 부여할 수도 있다.
    • 빈 이름은 중복되면 안 된다. (덮어씌워지거나, 오류 발생)
  3. 스프링 빈 저장소에 모든 빈이 등록되면, 스프링 컨테이너는 AppConfig.class를 바탕으로 의존관계를 주입(DI)한다.

스프링 컨테이너에 등록된 모든 빈 조회

스프링 컨테이너에 실제로 스프링 빈들이 잘 등록되었는지 확인하기 위한 테스트 코드를 작성한다.

ApplicationContextInfoText
package spring.basic.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.AppConfig;

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 Bean 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("Application Bean 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //ROLE_APPLICATION : 직접 등록한 Application Bean
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

    @Test
    @DisplayName("Infrastructure Bean 출력하기")
    void findInfrastructureBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 Bean
            if(beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }
}
  • getBeanDefinitionNames() 메소드로 스프링 컨테이너에 등록된 모든 빈 이름을 조회한다.
  • getBean() 메소드로 빈 이름으로 빈 객체를 조회한다.
  • getRole() 메소드로 빈 역할에 따른 빈 객체를 조회할 수 있다.

빈이 가질 수 있는 Role은 ROLE_APPLICATION(0), ROLE_SUPPORT(1), ROLE_INFRASTRUCTURE(2) 3가지다. APPLICATION은 사용자가 직접 등록한 빈이고, SUPPORT는 외부 라이브러리나 내부적으로 부요 빈을 돕는 설정/구성용 빈이고, INFRASTRUCTURE는 스프링 배우에서 사용하는 빈이다. 그리고 위 테스트 코드에서는 beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION 같이 비교하고 있는데, 괄호 안의 숫자처럼 beanDefinition.getRole() == 0으로 작성해도 비교문은 True로 통과한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 6  (0) 2025.04.27
Spring 스터디2 - 5  (1) 2025.04.19
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디2 - 1  (0) 2025.03.23

서비스 운영 중 일부 js 파일 등 static content의 노출로 인한 취약점 해결을 위해서 찾아보았다. 사용자의 직접 접근 요청을 차단하고, 설정한 referer의 경우에만 Directory에 접근하여 기능이 정상적으로 동작하도록 한다.

SetEnvIf Referer ^http(s)?:\/\/ allow_access
...
<Directory "/foo/bar">
    Require all denied
    Require env allow_access
</Directory>

 

Apache의 mod_setenvif 를 참고하면 SetEnvIf 지시어로 환경변수를 설정할 수 있다. 첫번째 인자는 attribute로 HTTP 요청 헤더나 Remote_Host, Remote_Addr 등을 지정하고, 두번째 인자로 정규표현식을 지정하는데 regex가 attribute에 대응하면 된다. 세번째 인자는 변수명이다.
 
따라서, 설정한 값은 HTTP 요청 헤더 중 Referer 헤더가 http:// 또는 https://로 시작하면 allow_access가 참이 된다. 그리고 VirtualHost에서 Directory 지시자로 지정한 경로(서버 기준의 절대경로)에 allow_access 대응했을 경우에만 접근이 가능하다. 목적은 직접 접근 요청 차단이어서 해결 완료(실제 서버에는 Referer 정규표현식을 더 상세하게 작성함).

'DevOps > WEB.WAS' 카테고리의 다른 글

[JBoss] EAP 7.4 → 8.0 datasource의 security 설정 문제  (1) 2025.05.02
[JBoss] JBWEB002004 오류 발생  (0) 2025.01.09

새로운 할인 정책 개발

기존에 주문금액에 관계없이 1,000원을 고정해서 할인해주던 정책을 주문금액의 10%를 할인해주는 정책으로 변경한다. 이전에 사용하던 FixDiscountPolicy 구현 클래스와 이번에 변경할 RateDiscountPolicy 구현 클래스를 나타낸 클래스 다이어그램이다.

RateDiscountPolicy와 정책을 테스트 하기 위한 테스트 코드를 작성한다.

RateDiscountPolicy
package spring.basic.discount;

import spring.basic.member.Grade;
import spring.basic.member.Member;

public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
RateDiscountPolicyTest
package spring.basic.discount;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import spring.basic.member.Grade;
import spring.basic.member.Member;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class RateDiscountPolicyTest {
    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")
    void vip_o() {
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다")
    void vip_x() {
        //given
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        assertThat(discount).isEqualTo(0);
    }
}

새로운 할인 정책 적용과 문제점

할인 정책 변경을 위해 클라이언트인 OrderServiceImpl을 수정한다.

public class OrderServiceImpl implements OrderService{
    ...
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    ...
}

인터페이스와 구현 클래스 분리로 다형성 활용 등 객체지향 설계 원칙을 준수한 것으로 보이지만 사실은 아니다. 현재 클라이언트인 OrderServiceImpl은 인터페이스인 DiscountPolicy를 의존하면서 구현 클래스인 FixDiscountPolicy도 의존하고 있었다. 그래서 할인 정책을 변경하기 위해 클라이언트를 수정할 수 밖에 없었다. 이는 DIP(추상화에만 의존)와 OCP(확장에만 열림)를 위반하게 된다. 위반한 원칙을 준수하기 위해 구현 클래스가 아닌 인터페이스에만 의존하도록 코드 수정해보았다.

public class OrderServiceImpl implements OrderService{
    ...
    //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private DiscountPolicy discountPolicy;
    ...
}

클라이언트에서 인터페이스만 있으면 실제 구현 클래스가 없어서 NullPointerException 오류가 발생한다.

관심사의 분리와 AppConfig

애플리케이션을 하나의 연극이라고 했을 때, 인터페이스는 배역이고 구현 클래스는 배우로 비유할 수 있다. 지금까지의 내용은 배우가 상대 배역의 배우까지 초빙하는 느낌이다. 배우는 배역에만 집중해야하고 배우의 초빙은 기획자가 진행하는게 인상적이다. 이와 같이 애플리케이션 동작을 구성하기 위해 AppConfig 클래스를 만든다.

AppConfig
package spring.basic;

import spring.basic.discount.FixDiscountPolicy;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;
import spring.basic.member.MemoryMemberRepository;
import spring.basic.order.OrderService;
import spring.basic.order.OrderServiceImpl;

public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

AppConfig가 실제 동작에 필요한 MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, FixDiscountPolicy 객체를 생성하고, 생성한 객체 인스턴스의 레퍼런스를 생성자를 통해 주입한다. 아직 각 클래스에 생성자가 없어 생성자를 작성해준다.

MemberServiceImpl
package spring.basic.member;

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

생성자를 작성하면서 위에서처럼 MemberRepository를 구현 클래스가 아닌 인터페이스만 의존하도록 수정한다. MemberServiceImpl 입장에서는 생성자를 통해 어떤 객체가 주입될 지 알 수 없고, 오직 AppConfig를 통해서 결정된다. 이렇게 객체의 생성과 연결을 AppConfig가 담당하면서 DIP를 준수하게 된다.
 
동일한 방식으로 OrderServiceImpl도 수정하고 AppConfig를 통해 애플리케이션이 실행될 수 있게 메인 메소드가 있는 MemberApp과 OrderApp, 테스트를 위한 MemberServiceTest와 OrderServiceTest를 수정한다.

MemberApp
package spring.basic;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.member.Grade;
import spring.basic.member.Member;
import spring.basic.member.MemberService;

public class MemberApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find member = " + findMember.getName());
    }
}
OrderApp
package spring.basic;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.member.Grade;
import spring.basic.member.Member;
import spring.basic.member.MemberService;
import spring.basic.order.Order;
import spring.basic.order.OrderService;

public class OrderApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 20000);

        System.out.println("order = " + order);
        System.out.println("order = " + order.calculatePrice());
    }
}
MemberServiceTest
package spring.basic.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spring.basic.AppConfig;

public class MemberServiceTest {
    MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

    @Test
    void join() {
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}
OrderServiceTest
package spring.basic.order;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spring.basic.AppConfig;
import spring.basic.member.Grade;
import spring.basic.member.Member;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;

public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

    @Test
    void createOrder() {
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

좋은 객체 지향 설계의 5가지 원칙의 적용

지금까지 SRP, DIP, OCP 3가지 원칙을 적용했다.

SRP(Single Responsibility Principle, 단일 책임 원칙)

클래스는 하나의 책임만을 가져야 한다.

  • AppConfig는 구현 객체를 생성하고 연결만 함
  • 클라이언트 객체는 실행만 함

DIP(Dependency Inversion Principle, 의존관계 역전 원칙)

고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 한다.

  • 처음에 OrderServiceImpl 에서는 인터페이스인 DiscountPolicy와 구현 클래스인 FixDiscountPolicy를 의존
  • 이를 인터페이스 DiscountPolicy만 의존하도록 변경
  • AppConfig에서 FixDiscountPolicy를 주입하도록 변경

OCP(Open-Closed Principle, 개방-폐쇄 원칙)

소프트웨어 요소(Entity)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.

  • DIP 준수를 위해 소스를 수정하면서 FixDiscountPolicy에서 RateDiscountPolicy로 변경을 AppConfig만 수정하면서 가능
  • 클라이언트는 수정할 필요가 없음

IoC와 DI

IoC(Inversion of Control, 제어의 역전)란 객체의 생성과 관리 책임을 개발자가 아닌 프레임워크가 담당한다. 전통적인 프로그래밍 방식에서는 개발자가 객체를 직접 생성하고 제어 흐름을 관리했지만, IoC에서는 프레임워크가 이 역할을 대신한다.

 

DI(Dependency Injection, 의존성 주입)란 IoC를 구현하는 구체적인 방법으로, 객체 간의 의존성을 외부에서 주입받는 디자인 패턴이다. DI를 통해 객체가 직접 다른 객체를 생성하거나 참조하지 않고, 컨테이너가 필요한 의존성을 주입해준다.

스프링으로 전환

AppConfig를 우선 스프링 기반으로 변경한다. @Configuration 어노테이션을 클래스에 붙여주고, @Bean 어노테이션을 각 메소드에 붙여준다.

AppConfig
package spring.basic;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.basic.discount.DiscountPolicy;
import spring.basic.discount.FixDiscountPolicy;
import spring.basic.discount.RateDiscountPolicy;
import spring.basic.member.MemberRepository;
import spring.basic.member.MemberService;
import spring.basic.member.MemberServiceImpl;
import spring.basic.member.MemoryMemberRepository;
import spring.basic.order.OrderService;
import spring.basic.order.OrderServiceImpl;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

MemberApp과 OrderApp도 스프링 기반으로 변경한다.

OrderApp
package spring.basic;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.basic.member.Grade;
import spring.basic.member.Member;
import spring.basic.member.MemberService;
import spring.basic.order.Order;
import spring.basic.order.OrderService;

public class OrderApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 20000);

        System.out.println("order = " + order);
        System.out.println("order = " + order.calculatePrice());
    }
}

OrderApp 기준으로 봤을 때 기존의 AppConfig를 불러오는 부분과, AppConfig를 통해서 MemberService와 OrderService를 불러오는 부분을 스프링 컨테이너인 ApplicationContext를 통해서 AppConfig를 불러와 사용한다. 스프링 컨테이너는 위에서 @Configuration 어노테이션이 있는 AppConfig 클래스를 설정 정보로 사용하고, @Bean 어노테이션이 있는 메소드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록하게 된다. 이를 스프링 빈이라고 한다.

스프링 빈은 기본적으로 @Bean 어노테이션이 붙은 메소드 명을 스프링 빈의 이름으로 사용하는데, 설정을 통해 이름을 변경해서 사용할 수도 있다. 스프링 빈은 applicationContext.getBean() 메소드를 통해 찾아서 사용할 수 있다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 5  (1) 2025.04.19
Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 7  (1) 2025.03.09

프로젝트 생성

스프링 프레임워크의 기능을 사용하지는 않을 거지만, 빠르게 프로젝트를 생성하기 위해 spring initializr를 통해 프로젝트를 생성한다.

조금 더 빠른 실행을 위해 IntelliJ 설정 > Gradle에서 빌드/테스트 실행 환경을 IntelliJ IDEA로 수정한다.

github remote repository 생성 후 해당 프로젝트를 연결했다.

# 프로젝트 디렉토리 루트 위치에서 실행
git init

# remote repository 설정(origin)
git remote add origin https://github.com/hongbre/spring-basic-1.git

# 사용 브랜치 master → main 변경
git branch -b main

# 변경 된 모든 파일 커밋 대상으로 추가
git add .

# 커밋 메시지와 함께 커밋
git commit -m "Project 생성"

# remote repository(github)에 push
git push origin main

비즈니스 요구사항

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원을 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용한다. (미확정)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고자 한다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)

미확정인 회원 데이터 저장 방법, 할인 정책은 언제든지 변경 가능하도록 인터페이스로 구현할 필요가 있다.

회원 도메인 설계 및 개발

회원 도메인 클래스 다이어그램

회원 도메인 개발

회원 등급을 가진 enum 타입의 Grade와 회원 엔티티를 가진 Member 클래스를 생성한다.

public enum Grade {
    BASIC,
    VIP
}
public class Member {
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

이후 MemberRepository 인터페이스를 생성하고, MemoryMemberRepository 구현 클래스를 생성한다.

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    ...
}

MemorymemberRepository에서 HashMap을 사용하고 있는데, 동시성 문제가 발생할 수 있으니 ConcurrentHashMap 사용을 권장한다고 한다. 이때 HashMap을 사용 할 때 동시성 문제가 발생하는 이유는 주로 여러 스레드가 동시에 맵을 수정하거나 접근할 때 발생하며, 데이터 무결성이 깨지는 문제이다. ConcurrentHashMap은 세그먼트 잠금(내부를 16개 세그먼트로 분할하여 부분적 잠금 사용), 원자적 메소드를 제공하여 복합 연상을 스레드 세이프하게 처리하는 등 스레드의 안전한 연산을 보장하고, Fail-Safe 반복자 사용으로 수정 중에도 안전하게 순회할 수 있다.

MemberService 인터페이스를 생성하고, MemberServiceImpl 구현 클래스를 생성, MemberApp를 생성해서 main 클래스를 추가해주는데 여기서 테스트를 진행하는 것은 바람직하지 못하니 JUnit 테스트 클래스를 생성해서 진행한다.

public class MemberServiceTest {
    MemberService memberService = new MemberServiceImpl();

    @Test
    void join() {
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

가입한 member와 findMember로 찾은 멤버가 같으면 테스트는 성공한다.

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    ...
}

여기서 문제가 있다면 Service 구현 클래스에서 인터페이스인 MemberRepository에도 의존하고, 구현 클래스인 MemorymemberRepository에도 의존하고 있다.

주문과 할인 정책 도메인 설계 및 개발

주문과 할인 정책 도메인 클래스 다이어그램

주문과 할인 정책 도메인 개발

memberId, itemName, itemPrice, discountPrice 4개의 속성을 가진 Order 클래스를 생성하고, OrderService 인터페이스와 OrderServiceImpl 구현 클래스 생성 및 DiscountPolicy 인터페이스와 FixDiscountPolicy 구현 클래스를 생성하게 된다. 동일하게 OrderApp 이라는 main 클래스를 추가해주었고, JUnit 테스트 클래스를 생성하였다.

public class OrderServiceTest {
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder() {
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

memberA를 VIP 등급으로 회원 가입 시키고, 10000원짜리 itemA를 주문했을 때 할인 금액이 1000원이면 테스트는 성공한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 4  (1) 2025.04.17
Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18

스터디 시작

김영한 Spring 로드맵의 두 번째인 스프링 핵심 원리 - 기본편 강의를 보며 각자의 블로그에 정리하고 이를 교환 해 서로의 지식을 공유하기로 했다.

스프링(Spring)이란?

2002년, EJB(Enterprise Java Beans)를 사용하던 개발자 중 한 명인 로드 존슨(Rod Johnson)이 EJB의 문제점을 지적한 책을 출간하였다. EJB 없이도 고품질의 확장 가능한 애플리케이션 개발이 가능함을 보여주고 있어 책이 유명해졌고, 유겐 휠러(Juergen Hoeller)와 얀 카로프(Yann Caroff)가 로드 존슨에게 오픈소스 프로젝트를 제안하며 시작되었다고 한다. 스프링의 핵심 코드의 상당수는 현재도 유겐 휠러가 개발하고 있고, 여담으로 이름의 유래는 EJB(J2EE)라는 겨울을 넘어 새로운 시작(봄)이라는 뜻으로 지었다고 한다.
 


spring.io 내 projects 화면을 켭쳐했다. 핵심이 되는 Spring Boot와 Spring Framework가 제일 위에 나오고, Spring Data, Cloud, Security 등 24개의 프로젝트(작성일 기준)가 존재한다.

Spring Boot

Spring Boot는 Spring Framework를 기반으로 빠르게 애플리케이션을 구축할 수 있는 프레임워크이다. Spring Boot는 클래스 패스와 빈 설정을 분석하여 필요한 구성 요소를 자동으로 추가하여 환경(Infrastructure) 설정에 대한 부담을 줄인다. 이를 통해 개발자는 비즈니스 로직에 집중할 수 있다. Spring Boot는 Starter Projects와 Auto-Configuration을 통해 쉽게 마이크로 서비스를 개발할 수 있도록 지원한다.

Spring Framework

Spring Framework는 Enterprise Java 애플리케이션을 구축하기 위한 오픈소스 프레임워크이다. AOP(Aspect-Oriented Programming), 스프링 DI 컨테이너(Dependency Injection) 같은 기술을 제공하여 복잡한 개발을 단순화한다. 또한 Spring Framework는 IoC(Inversion of Control) 컨테이너를 통해 Java 객체의 생명주기를 관리하고, Core 컨테이너는 Core, Beans, Context 모듈로 구성되어있다.

다른 프로젝트들

  • Spring Data : 다양한 데이터베이스에 일관된 접근 방식을 제공한다.
  • Spring Cloud : 분산 시스템에서 공통 패턴을 쉽게 구축할 수 있도록 도와준다.
  • Spring Security : 인증 및 권한 부여를 관리하며, 보안 강화를 위해 사용한다.
  • Spring Integration : 이벤트 기반 액션을 처리하는데 적합하며, 데이터 레이어 위에서 이벤트 캡쳐 및 처리가 가능하다.
     
    강의에서는 핵심 개념을 이해하는 것이 해당 기술을 이해하고 사용하는데 가장 중요하다고 얘기한다. 이 기술을 왜 만들었는지? 이 기술의 핵심 컨셉이 무엇인지? 이다. 스프링은 Java 기반의 프레임워크로, Java의 가장 큰 특징으로는 객체 지향 언어인 것이다. 따라서 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내어 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크라는 것이 스프링의 핵심 개념이다.

좋은 객체 지향 프로그래밍이란?

우선 객체 지향 프로그래밍(OOP, Object-Oriented Programming)의 특징이다.

  1. 캡슐화(Encapsulation) : 데이터와 코드의 형태를 외부로부터 숨기고, 데이터의 구조와 역할, 기능을 하나의 단위로 묶는 방법이다. 이는 데이터의 무결성을 유지하고, 객체 간의 상호 작용을 명확히 정의한다.
  2. 추상화(Abstraction) : 복잡한 시스템의 핵심적인 특성만을 드러내고, 불필요한 세부 사항은 숨기는 방법이다. 이를 통해 시스템의 복잡성을 줄이고 이해하기 쉽게 만든다.
  3. 상속(Inheritance) : 새로운 클래스가 기존 클래스의 속성과 메서드를 물려받아 사용하는 기능이다. 이를 통해 코드의 재사용성을 높이고, 계층 구조를 형성할 수 있다.
  4. 다형성(Polymorphism) : 객체가 다양한 형태로 표현될 수 있는 능력이다. 이는 오버로딩(Overloading)과 오버라이딩(Overriding)을 통해 구현된다.

위 특성들을 바탕으로 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만든다. 여기서 '유연하고 변경이 용이'는 레고를 조립하듯, 키보드를 교체하듯이 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법을 말한다. 여기서 자연스럽게 특징 중 하나인 다형성(Polymorphism)에 집중하게 된다.

다형성의 실세계 비유

물론 실세계와 객체 지향을 1:1로 매칭하는 것을 불가하다. 하지만 역할과 구현으로 세상을 구분하면 다형성을 이해하기 보다 쉽다.

운전자 역할과 자동차 역할로 구분했을 때, 자동차 구현인 차A가 자동차 역할을 하든 차Z가 자동차 역할을 하든 운전자 역할에서 봤을 때는 자동차 역할이다. 어떤 자동차가 와도 운전을 할 수 있는 것이다. 이와 같이 역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해진다. 장점으로는 클라이언트가 대상의 역할(인터페이스)만 알면 되고, 구현체의 내부 구조를 몰라도 되고, 내부 구조가 변경되어도 영향이 없으며, 구현체 자체를 변경해도 영향을 받지 않는다는 것이다.
 
Java의 다형성을 활용하면 역할은 곧 인터페이스(Interface)이며, 구현은 인터페이스를 통해 구현한 클래스나 구현 객체를 의미한다. 이는 객체를 설계할 때 역할과 구현을 명확히 분리해야 한다는 말로, 역할(인터페이스)을 먼저 부여하고 그 역할을 수행하는 구현 객체를 만들어야 한다. 혼자 있는 객체는 없으며 수많은 객체 클라이언트(요청)와 객체 서버(응답)는 서로 협력 관계를 가진다.

Java의 다형성

메서드 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 동일한 이름, 매개변수, 반환 타입으로 재정의하는 것을 의미한다. 이를 통해 자식 클래스는 부모 클래스의 메서드와 같은 이름을 가지지만, 다른 구현을 제공할 수 있다. 이런 메서드 오버라이딩은 다형성을 구현하는 중요한 방법 중 하나이다.

입문 스터디 당시에 구현했던 MemberRepository이다. 이때도 MemberRepository 인터페이스를 통해 MemoryMemberRepository, JdbcMemberRepository 등 다양한 데이터 저장 방식으로 클래스를 구현했었다. MemberService가 클라이언트 역할이고 MemberRepository가 서버 역할일 때, 클라이언트를 변경하지 않고 서버의 기능을 유현하게 변경할 수 있었다.
 
이런들 저런들 역할과 구현이라는 컨셉을 다형성을 통해 객체 지향으로 구현할 수 있으며, 이는 유연하고 변경이 용이한, 확장 가능한 설계가 된다. 변경 시 클라이언트에 영향을 주지 않기 위해서는 인터페이스를 안정적으로 설계하는 것이 중요하다. 물론 인터페이스 자체가 변하게 되면 큰 변경이 발생한다는 한계도 있다.

SOLID 원칙

객체 지향 설계의 SOLID 원칙은 소프트웨어를 더 유지보수하기 쉽고 확장 가능한 구조로 만드는데 도음을 주는 다섯 가지 기본 원칙이다. SOLID는 SRP, OCP, LSP, ISP, DIP의 첫 글자를 따서 만든 약어이다.

  1. SRP(Single Responsibility Principle, 단일 책임 원칙)
    • 클래스는 하나의 책임만을 가져야 한다. 즉, 클래스는 하나의 이유로만 변경되어야 한다.
    • 하나의 책임이라는 것은 모호하나, 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 SRP 원칙을 잘 따른 것이다.
  2. OCP(Open-Closed Principle, 개방-폐쇄 원칙)
    • 소프트웨어 요소(Entity)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.
    • 다형성, 위에서 얘기한 역할과 구현의 분리를 생각할 수 있다.
    • 입문 스터디에서 구현했을 당시에는 MemberService가 구현 클래스를 직접하며 코드를 변경했었다.
      • 다형성(인터페이스를 통한 클래스 구현)은 사용 했으나 OCP 원칙은 지키지 못했다.
      • 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요하며 이는 추후에 배운다.
  3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙)
    • 부모 클래스의 객체는 자식 클래스의 객체로 대체될 수 있어야 한다.
    • 위 자동차를 예로 들면 엑셀은 앞으로 가라는 기능인데, 뒤로 가게 구현하면 LSP 원칙 위반이다.
      • 즉, 인터페이스로 구현한 클래스는 인터페이스에 정의된 규약은 다 지켜아한다.
  4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)
    • 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 한다.
      • 클라이언트가 불필요한 메서드나 속성에 의존하지 않도록 하여 코드의 유지보수성과 유연성을 높이는 데 도움을 준다.
      • 하나의 범용 인터페이스를 사용하기 보다는 여러 개의 특정한 인터페이스를 사용하는 것이 더 좋다.
  5. DIP(Dependency Inversion Principle, 의존관계 역전 원칙)
    • 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 둘 다 추상화에 의존해야 한다.
      • 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다.
      • 위에서 얘기한 구현과 역할에서 역할에 의존하라는 말이다. 차A에 의존하게 되면 자동차를 바꾸기 어렵지만, 자동차에 의존하면 쉽게 바꿀 수 있다.
    • OCP와 같이 MemberService는 인터페이스에 의존하지만, 구현 클래스도 의존하고 있다.
      • 코드 내에서 직접 구현 클래스를 선택하기 때문에 DIP 원칙 위반이다.

객체 지향의 핵심은 다형성이지만, 다형성만으로는 OCP, DIP를 지킬 수 없다.

객체 지향 설계와 스프링

스프링은 스프링 DI 컨테이너를 통해 다형성을 유지하며 OCP, DIP를 준수할 수 있다. 스프링 DI 컨테이너는 객체 간 의존성을 런타임 시에 주입하는데, 이를 통해 인터페이스를 정의하고 다양한 구현체를 주입받기 때문이다. 이렇게 스프링으로 개발하게 되면 클라이언트 코드의 변경 없이 기능 확장이 가능하게 된다.
 
물론 이론적인 내용으로 DI를 완전히 이해하기는 어렵다. 이제 실제 코드를 작성해보면서 이해해야겠다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 3  (0) 2025.04.13
Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17

AOP

AOP(Aspect-Oriented Programming)는 공통 관심 사항(Cross-Cutting Concerns)을 모듈화하여 관리한다.

회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항(Core Concerns)가 아니고, 시간을 측정하는 로직은 공통 관심 사항이다. 하지만 각 기능에 시간을 측정하는 기능을 넣을 경우 비즈니스 로직이 섞여 유지보수가 어렵고, 시간을 측정하는 기능에 수정이 발생할 때 모든 부분을 수정해야해서 불편하다.

package hello.hello_spring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeTraceAop {
    @Around("execution(* hello.hello_spring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());
        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs +
                    "ms");
        }
    }
}

TimeTraceAop를 작성한다. @Aspect 어노테이션으로 AOP임을 나타내고, @Around 어노테이션으로 적용될 클래스를 명시한다. 스프링 빈에 올리기 위해 @Component 어노테이션을 사용한다. 이렇게 핵심 관심 사항과 공통 관심 사항을 분리할 수 있다.

AOP 적용 전에는 memberController가 memberService를 호출했다면, AOP 적용 후에는 memberController가 프록시 memberService(복제 된)를 호출하고, 프록시 memberService가 joinPoint.proceed() 메소드를 통해 실제 memberService를 호출하게 된다.


AOP를 마지막으로 스프링 입문 강의가 끝났다. 이번에 한 AOP도 그렇고 자세하게 공부할 것들이 많다...

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 2  (0) 2025.03.30
Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16

H2 데이터베이스 설치

H2 공식 홈페이지

Windows Installer를 다운로드 받아 설치한다.

H2 Console을 클릭하여 H2 데이터베이스를 실행한다.

~/test로 연결 하면 Windows 기준 USERPROFILE 경로에 test.mv.db가 생긴다.

/* DROP TABLE IF EXISTS MEMBER CASCADE; */
CREATE TABLE MEMBER
(
    ID  BIGINT GENERATED BY DEFAULT AS IDENTITY,
    NAME VARCHAR(255),
    PRIMARY KEY (ID)
);

INSERT INTO MEMBER(NAME) VALUES('member1');
INSERT INTO MEMBER(NAME) VALUES('member2');
INSERT INTO MEMBER(NAME) VALUES(666, '666member');

member 테이블 생성하고 데이터를 삽입한다. ID는 BIGINT 타입으로 지정하고, 값을 입력하지 않았을 때 GENERATED BY DEFAULT AS IDENTITY가 자동으로 값을 입력해준다. NAME은 VARCHAR 타입으로 255자까지 가변적으로 입력가능한 문자열이다. PK로 ID를 지정하여 고유한 값을 갖게 된다.

좌측 메뉴에서 테이블명을 선택하면 자동으로 Select문이 생성되어 쉽게 조회 할 수 있다. 방금 삽입한 3개의 member가 조회된다.

순수 JDBC

이제는 사용하지 않는 방식이라고 넘어갔지만 코드 작성해서 실행해보았다.

@Configuration
public class SpringConfig {

    @Autowired
    private final DataSource dataSource;

    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }

}

SpringConfig에 DataSource를 추가해주고, memberRepository에서 작성한 JdbcMemberRepository로 변경한다. DataSource는 데이터베이스와의 커넥션을 획득할 때 사용하는 객체로, 스프링 부트에서는 정보(application.properties)를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 두기 때문에 의존성 주입(DI)을 받을 수 있다.

스프링 통합 테스트

MemberServiceIntegrationTest
package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

이전에 했던 단위 테스트 말고 통합 테스트를 위한 MemberServiceIntegrationTest 클래스 파일을 작성한다. @SpringBootTest 어노테이션으로 스프링 컨테이너를 테스트와 함께 실행하도록 하며, @Transactional 어노테이션으로 테스트 트랜잭션 시작 후 종료 시에 전부 롤백하도록 한다. @Transactional 어노테이션이 없으면 데이터베이스 PK 문제 발생 시 테스트 케이스를 계속 바꿔줘야할 수 있다.

스프링 JdbcTemplate

스프링 JdbcTemplate는 JDBC 코드의 반복적인 부분을 줄여준다.

JdbcTemplateMemberRepository
//import 생략
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }
    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

이후에 Spring Config에서 JdbcTemplateMemberRepository를 사용하도록 수정해준다.

//생략
    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }

JPA

JPA는 자바에서 ORM(Object-Relational Mapping, 객체-관계 매핑)을 위한 표준 인터페이스로, 자바 객체와 관계형 데이터베이스 간의 매핑을 쉽게 하여 개발자가 SQL 쿼리를 작성하지 않아도 된다. (인터페이스이므로 실제 구현이 필요하다. 대표적으로 Hibernate, EclipseLink, OpenJPA 등이 있다.)

주요 기능으로는 객체 중심의 개발(쿼리보다 자바 객체에 집중하여 개발 생산성 향상과 유지보수 용이), CRUD 작업 자동화, ORM 기능(자바 객체와 데이터베이스 테이블 간의 매핑 제공으로 패러다임 불일치 해결)이 있고, 주요 구성 요소로는 Entity(데이터베이스 테이블과 매핑되는 자바 클래스, @Entity 어노테이션), Entity Manager(Entity의 생명주기를 관리하는 인터페이스로 CRUD 작업 수행), Persistent Context(영속성 컨텍스트, Entity Manager가 관리하는 Entity 인스턴스의 집합으로 데이터베이스와의 동기화를 도움)가 있다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

우선 build.gradle 파일에 jpa를 추가해준다. jpa 라이브러리 안에 jdbc를 포함하므로 기존에 사용했던 jdbc는 제외한다.

//생략
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

그리고 application.properties에 옵션을 추가해준다. show-sql은 jpa가 생성한 쿼리를 보여주고, ddl-auto는 jpa가 자동으로 테이블을 생성할지 여부이다. 이미 생성된 테이블을 사용하고 있어서 none으로 설정했지만, create로 하면 테이블을 생성해준다.

//생략
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

Member 도메인을 Entity로 사용하기 위해 @Entity 어노테이션을 설정해주고, @Id 어노테이션으로 PK를 지정, @GeneratedValue 어노테이션으로 값을 자동으로 생성해준다.

JpaMemberRepository
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        return result.stream().findAny();
    }
}

JpaMemberRepository를 작성한다. DataSource를 받아오는게 아니라 EntityManager를 불러와서 사용한다.

//생략
@Transactional
public class MemberService {

`MemberService'에 @Transactional 어노테이션을 달아준다. jpa의 모든 쿼리는 트랙잭션 내에서 실행되어야 한다.

//생략
@Configuration
public class SpringConfig {

    private final DataSource dataSource;
    private final EntityManager em;

    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }

}

SpringConfig에서 JpaMemberRepository를 사용하도록 변경한다. EntityManager를 추가한다. 이후 테스트를 돌리거나 실행하면 정상적으로 완료된다. jpa는 이런 쿼리를 생성했다.

스프링 데이터 JPA

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

SpringDataJpaMemberRepository를 작성한다. 클래스가 아닌 인터페이스로 작성하고, JpaRepository를 상속하게 되면 스프링 실행 시 자동으로 스프링 빈에 등록되어 사용할 수 있게된다.

//생략
@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

SpringConfig에서 MemberRepository만 의존성 주입 받으면 된다.

스프링 데이터 JPA는 JPA 기반으로 구축되었고, 기본적인 CRUD 기능도 대부분 제공한다. findByID, findByAll 등등. 우선 강의에 있어 스프링 데이터 JPA를 사용해보았으나, 우선 JPA에 대한 이해도를 높일 필요가 있어 보이고, 결국 모든 쿼리를 JPA로 처리할 수 없어서 네이티브 쿼리나 JdbcTemplate과의 조합 등의 사용도 고려해야한다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디2 - 1  (0) 2025.03.23
Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12

홈 화면 추가

HomeController.java와 home.html을 각각 생성한다.

HomeController.java
...
@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}
home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <h1>Hello Spring</h1>
    <p>회원 기능</p>
    <p>
      <a href="/members/new">회원 가입</a>
      <a href="/members">회원 목록</a>
    </p>
  </div>
</body>
</html>

빌드하고 localhost:8080 접속하면 위 화면이 나온다. 지난 번에 static/index.html 파일을 만들어서 Controller에 루트 컨텍스트("/")가 없으면 해당 파일을 보여주도록 했으나, 이번에는 Controller에 루트 컨텍스트를 지정하여 template/home.html 파일을 보여준다. 두 버튼에 대해서는 아직 화면이 없어서 오류 화면으로 넘어간다.

등록 화면 추가

MemberController.java
...
    @GetMapping("members/new")
    public String createForm() {
        return "members/createMemberForm";
    }

    @PostMapping("members/new")
    public String create(MemberForm form) {
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }
...
createMemberForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <form action="/members/new" method="post">
      <div class="form-group">
        <label for="name">이름</label>
        <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
      </div>
      <button type="submit">등록</button>
    </form>
  </div>
</body>
</html>
MemberForm.java
...
public class MemberForm {
    private String name;

    public String getName() {
        return name;
    }

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

홈 화면에서 회원 가입을 클릭하면 방금 만든 등록 화면(template/createMemberForm.html)으로 이동한다. 이동하면 위와 같이 이름을 입력하고 등록할 수 있는 버튼이 생긴다. 여기서 form을 활용하게 되는데, form 태그에 명시한대로 등록(submit) 버튼을 클릭하면 POST 메소드를 사용해 /members/new를 호출한다. MemberController에서 @PostMapping 어노테이션을 달고 있는 메소드를 찾고, 실행한다. MemberForm에 private로 생성한 name에 input 태그에 명시한 name 프로퍼티의 값이 name으로 같을 때 해당 MemberForm 데이터로 들어가고, 이 name으로 member를 생성, 홈 화면으로 돌아간다.

조회 화면 추가

MemberController.java
...
   @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
memberList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div class="container">
    <table>
      <thead>
      <tr>
        <th>#</th>
        <th>이름</th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="member : ${members}">
        <td th:text="${member.id}"></td>
        <td th:text="${member.name}"></td>
      </tr>
      </tbody>
    </table>
  </div>
</body>
</html>

메모리에 저장하다보니 재시작 하면 목록에 아무것도 안 나온다.

등록 화면에서 회원을 등록하면 위 처럼 id와 name이 나온다. 조회 화면에 들어오면 findMembers() 메소드로 회원 목록을 조회하고, List를 Model에 추가하여 memberList.html로 전달한다. 그럼 thymeleaf가 th:each로 members List를 순회하며 member의 id와 name을 반복하여 출력하는 형태이다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 7  (1) 2025.03.09
Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 4  (0) 2025.02.16
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 2  (0) 2025.02.10

컴포넌트 스캔과 자동 의존관계 설정

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

MemberController 파일을 작성하고 잘 동작하는지 실행해보면 Parameter 0 of constructor in hello.hello_spring.controller.MemberController required a bean of type 'hello.hello_spring.service.MemberService' that could not be found.라는 오류가 발생한다. @Controller 어노테이션을 달고 있는 MemberController는 스프링 컨테이너가 시작 될 때 객체를 생성하지만, MemberService는 생성하지 않아서 발생하는 오류이다. 스프링 빈에 등록한다고 하는데, 스프링 빈에 등록하는 방법은 2가지가 있다.

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기
@Service
public class MemberService {
...
}

MemberService 앞에 @Service 어노테이션을 달면 스프링 빈에 등록된다. MemoryMemberRepository도 마찬가지고 @Repository 어노테이션을 등록하면 된다. 원래는 @Component 어노테이션이 스캔 대상이지만, @Controller, @Service, @Repository 어노테이션 안에도 @Component 어노테이션을 포함하고 있어서 스캔 대상(스프링 빈에 등록)에 포함된다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ...
}

MemberService 생성자에 @Autowired 어노테이션을 달면 객체 생성 시점에 스프링 컨테이너가 스프링 빈에서 내용을 찾아 의존성을 주입한다. 참고로 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다. 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정을 통해 싱글톤이 아니게 할 수 있으나, 특별한 경우를 제외 대부분 싱글톤을 사용한다.
 
싱글톤 패턴
 
클래스의 인스턴스를 하나만 생성하고, 이를 전역으로 공유하여 사용하는 디자인 패턴

  • 특징 : 인스턴스의 유일성 보장, 전역 접근 가능, 메모리 효율성(장점과 유사)
  • 단점 : 높은 결합도, 멀티스레드 문제, 테스트 어려움

스프링 빈
 
스프링 컨테이너에 의해 관리되는 Java 객체를 의미한다. 일반적인 Java 객체와 동일하지만, 스프링 컨테이너가 객체의 생성, 초기화, 의존성 주입, 소멸 등 생명 주기를 관리한다는 점에서 차이가 있다.

자바 코드로 직접 스프링 빈 등록하기

MemberService와 MemoryMemberRepository에 등록한 @Service, @Repository, @Autowired 어노테이션 제거 후 SpringConfig 클래스 작성한다.

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

@Configuration 어노테이션으로 시작하고, 각 스프링 빈에 추가할 객체에 대해서 @Bean 어노테이션을 달고 작성해준다. 이전에 어떤 형태로 데이터를 저장할지 정해진게 없어 Repository는 인터페이스 형태로 추가하였고, 현재는 MemoryMemberRepository를 사용하고 있다. 나중에 DB나 다른 형태로 변경 될 때 SpringConfig에서 내용만 바꿔주면 된다.

'스터디 > Spring' 카테고리의 다른 글

Spring 스터디 - 6  (0) 2025.02.18
Spring 스터디 - 5  (0) 2025.02.17
Spring 스터디 - 3  (0) 2025.02.12
Spring 스터디 - 2  (0) 2025.02.10
Spring 스터디 - 1  (0) 2025.02.04

+ Recent posts