728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

오늘은 싱글톤에 대해서 공부한다.

 

만약 스프링 없이 AppConfig를 사용한다면

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.customer.CustomerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        CustomerService customerService1 = appConfig.customerService();
        CustomerService customerService2 = appConfig.customerService();

        System.out.println("CustomerService1 = " + customerService1);
        System.out.println("CustomerService2 = " + customerService2);

        Assertions.assertNotSame(customerService1, customerService2);
    }
}

AppConfig를 요청할 때마다 객체를 새로 만들어야 한다.

그럴 때는 객체를 딱 1개만 생성하고 공유하는 싱글톤을 사용해야 한다.

 

package hello.core.singleton;

public class SingletonService {
    
    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    
    //2. public 으로 객체가 필요하면 해당 메서드로 받아갈 수 있도록 만든다.
    public static SingletonService getInstance(){
        return instance;
    }
    
    //3. 생성자를 private 으로 만들어서 생성할 수 없도록 막는다.
    private SingletonService(){
    }
    
    public void printMethod(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

이렇게 생성자를 private으로 막고 class 내의 메서드를 이용해 객체를 받아갈 수 있도록 만드는 것을 싱글톤이라고 한다.

 

이 싱글톤을 테스트해보면

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    public void singletonServiceTest(){

        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        Assertions.assertSame(singletonService1, singletonService2);
    }

두 객체가 같은 것을 확인할 수 있다.

하지만 싱글톤을 사용하면 싱글톤을 구현하는 코드 자체를 만들어야 하고, 클라이언트가 추상화가 아닌 싱글톤 구체 클래스에 의존하기에 DIP를 위반한다 등등 의 문제가 있기 때문에 다른 방법을 사용한다.

 

싱글톤의 문제를 해결한 방법이 스프링 빈으로 만든 빈이다.

스프링 컨테이너는 싱글톤 코드를 따로 작성하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.

그리고 DIP, OCP 등의 원칙을 위반하지 않고 코드를 작성할 수 있다.

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        CustomerService customerService1 = applicationContext.getBean("customerService", CustomerService.class);
        CustomerService customerService2 = applicationContext.getBean("customerService", CustomerService.class);

        System.out.println("customerService1 = " + customerService1);
        System.out.println("customerService2 = " + customerService2);
        
        Assertions.assertSame(customerService1, customerService2);
    }

이렇게 싱글톤 코드를 따로 작성하지 않아도

같은 객체를 공유하는 것을 볼 수 있다.

 

이렇게 좋은 싱글톤에도 문제점이 있다.

바로 공유를 하기 때문에 생기는 문제인데, 싱글톤은 상태를 유지하게 설계하면 안된다.

특정 클라이언트에서 값을 변경할 수 있도록 만들면 안되며, 가급적 읽기만 가능해야 한다.

package hello.core.singleton;

public class StatefulService {

    private int pay;

    public void order(String name, int pay){
        System.out.println("name = " + name + " price = " + pay);
        this.pay = pay;
    }
    
    public int getPay(){
        return pay;
    }
}

이런 class를 만들고

package hello.core.singleton;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    static class TestConfig{
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }
    }

    @Test
    void statefulServiceSingleton(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = applicationContext.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = applicationContext.getBean("statefulService", StatefulService.class);

        statefulService1.order("A", 1000);
        statefulService2.order("B", 2000);

        Assertions.assertNotEquals(1000, statefulService1.getPay());
    }
}

이렇게 싱글톤으로 공유를 해보면 A에서 1000이 나와야 하지만 나오지 않는 것을 볼 수 있다.

A가 값을 얻는 중간에 B에서 값을 변경했기 때문이다.

이렇게 싱글톤은 값을 변경할 수 있도록 설정을 하면 안된다.

 

다시 AppConfig 코드를 보자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


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

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

CustomerService에서 customerRepository를 호출한다.

이 customerRepository는 new MemoryCustomerRepository()를 호출한다.

orderService도 customerRepository를 호출한다.

이 customerRepository도 new MemoryCustomerRepository()를 호출한다.

 

이렇게 MemoryCustomerRepository를 계속 호출하면 싱글톤이 깨지는 것처럼 보인다.

 

한번 테스트를 해보자.

CustomerServiceImpl과 orderServiceImpl에 가지고 있는 CustomerRepository를 반환하는 메서드를 추가한다.

package hello.core.customer;

public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}
package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;

public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

 

그리고 이 class 들이 가지고 있는 customeRepository가 동일한지 확인해보자.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

    @Test
    void ConfigurationTest(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        CustomerServiceImpl customerService = applicationContext.getBean("customerService", CustomerServiceImpl.class);
        OrderServiceImpl orderService = applicationContext.getBean("orderService", OrderServiceImpl.class);
        CustomerRepository customerRepository = applicationContext.getBean("customerRepository", CustomerRepository.class);

        Assertions.assertSame(customerService.customerRepository(), customerRepository);
        Assertions.assertSame(orderService.customerRepository(), customerRepository);
    }
}

이 테스트 코드를 실행해보면 모두 동일한 인스턴스를 반환하는 것을 볼 수 있다.

 

이번엔 AppConfig에 코드를 추가해 생성자가 호출될 때마다 로그를 남기도록 해보자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        System.out.println("call AppConfig.customerService");
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        System.out.println("call AppConfig.customerRepository");
        return new MemoryCustomerRepository();
    }

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

이렇게 작성을 하고 테스트를 실행하면

 

call AppConfig.customerService
20:09:15.392 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'customerRepository'
call AppConfig.customerRepository
20:09:15.393 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
20:09:15.394 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'

 

이렇게 call은 customerService, orderService, customerRepository 각각 한번씩, 총 3번 실행되는 것을 볼 수 있다.

 

자바 코드 상으로는 customerRepository가 3번 실행되어야 하지만 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.

    @Test
    void configurationDeep(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        AppConfig bean = applicationContext.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());

        AppConfig appConfig = new AppConfig();

        System.out.println("AppConfig = " + appConfig.getClass());

    }

이렇게 스프링 빈을 조회해보면

 

bean = class hello.core.AppConfig$$SpringCGLIB$$0
AppConfig = class hello.core.AppConfig

 

빈으로는 AppConfig가 아니라 SpringCGLIB가 더 붙어 컨테이너에 등록이 되게 된다.

 

스프링이 해당 class를 상속받는 다른 클래스를 만들고 다른 클래스를 스프링 빈으로 등록한 것이다.

그렇기에 스프링이 싱글톤이 보장되도록 해준다.

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 업승면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저번 시간에 스프링 컨테이너를 만들어보았다.

 

스프링 컨테이너의 생성 과정에 대해 알아보자.

저번 시간에는 구성 정보로 AppConfig.class 를 넘겨주었다.

 

그러면 이 class 정보로 스프링 빈들을 등록한다.

AppConfig가 이렇게 구성이 되어 있었으니

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


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

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

스프링 빈 저장소에는

빈 이름 빈 객체
customerService CustomerServiceImpl....
orderService OrderServiceImpl....
customerRepository MemoryCustomerRepository....
discountPolicy RateDiscountPolicy....

로 스프링 빈이 등록이 된다.

빈 이름은 직접 등록할 수 있으며, 빈 이름끼리 겹치면 안된다.

 

스프링 빈들을 등록하고 나면 생성자들을 보고 의존관계를 파악한다.

이렇게 의존관계를 주입할 때 단순히 자바코드를 호출하는 것이 아닌 싱글콘으로 주입을 한다.

 

그러면 이렇게 만든 스프링 빈들을 조회해보자.

 

테스트에 패키지로 beanfind 패키지를 만들고

package hello.core.beanfind;

import hello.core.AppConfig;
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;

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

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

    //ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
    //ROLE_INFRASTRUCTURE : 스프링 내부에서 사용하는 빈

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition beanDefinition = applicationContext.getBeanDefinition(beanDefinitionName);

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = applicationContext.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + ", object = " + bean);
            }
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findNotApplicationBean(){
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition beanDefinition = applicationContext.getBeanDefinition(beanDefinitionName);

            if(beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE){
                Object bean = applicationContext.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + ", object = " + bean);
            }
        }
    }
}

하나씩 동작시켜보면 등록된 빈들이 출력되는 것을 볼 수 있다.

getBeanDefinitionNames()를 사용하면 스프링에 등록된 모든 빈 이름을 조회한다. 문자열 배열로 반환이 된다.

getBean(String)은 빈 이름으로 빈 객체를 조회한다.

 

이 getBean이 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 방법인데

getBean(빈 이름, 타입)

getBean(타입)

이렇게 2가지 방법이 있다.

 

빈 이름을 조회하는 예제를 만들어보자.

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름, 타입으로 조회")
    void findBeanByName(){
        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("타입으로만 조회")
    void findBeanByType(){
        CustomerService customerService = applicationContext.getBean(CustomerService.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2(){
        CustomerServiceImpl customerService = applicationContext.getBean("customerService", CustomerServiceImpl.class);

        Assertions.assertInstanceOf(CustomerServiceImpl.class, customerService);
    }

    @Test
    @DisplayName("빈 이름으로 조회 실패")
    void findBeanByNameFail(){
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () -> applicationContext.getBean("abcd", CustomerServiceImpl.class));
    }
}

이렇게 조회할 수 있고, 보통은 interface를 조회를 하는데 구체 타입으로 조회하면 변경시 유연성이 떨어지기 때문이다.

 

위의 2가지 방법을 이용해 조회할 수 있도록 하자.

 

만약 동일한 타입이 둘 이상이라면 어떻게 될까?

package hello.core.beanfind;

import hello.core.customer.CustomerRepository;
import hello.core.customer.MemoryCustomerRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

public class ApplicationContextSameBeanFindTest {

    @Configuration
    static class SameBeanConfig{

        @Bean
        public CustomerRepository customerRepository1(){
            return new MemoryCustomerRepository();
        }

        @Bean
        public CustomerRepository customerRepository2(){
            return new MemoryCustomerRepository();
        }
    }

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 오류가 발생한다.")
    void findBeanByTypeDuplicate(){
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () -> applicationContext.getBean(CustomerRepository.class));
    }

    @Test
    @DisplayName("그렇기 때문에 같은 타입이 둘 이상 있으면, 빈 이름을 지정해야 한다.")
    void findBeanByName(){
        CustomerRepository customerRepository = applicationContext.getBean("customerRepository1", CustomerRepository.class);

        Assertions.assertInstanceOf(CustomerRepository.class, customerRepository);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType(){
        Map<String, CustomerRepository> beansOfType = applicationContext.getBeansOfType(CustomerRepository.class);
        for(String key : beansOfType.keySet()){
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
        }

        System.out.println("beansOfType = " + beansOfType);
    }
}

당연히 같은 타입이 둘 이상 있으면, 오류가 발생하고 이름을 지정해주어야 한다.

만약 해당 타입에 있는 모든 빈을 조회하려면 getBeansOfType(타입)을 사용해야 한다.

 

스프링 빈을 조회할 때, 부모 타입으로 조회하면 해당 타입의 자식 타입까지 모두 조회한다.

package hello.core.beanfind;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

public class ApplicationContextExtendsFindTest {

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

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

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestConfig.class);

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

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findBeanByParentTypeBeanName(){
        DiscountPolicy rateDiscountPolicy = applicationContext.getBean("rateDiscountPolicy", DiscountPolicy.class);
        Assertions.assertInstanceOf(RateDiscountPolicy.class, rateDiscountPolicy);
    }

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

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

그렇기에 자식 타입이 둘 이상이라면 빈 이름을 사용해서 조회해야 한다.

 

스프링 컨테이너는 자바 코드 말고도 다양한 형식의 설정 정보를 받아드릴 수 있게 설계되어 있다.

지금까지 사용한 방법이 가장 왼쪽의 자바 코드를 사용하는 방법이었고 이외에도 많이 사용하지는 않지만 XML을 사용하는 방법 등도 있다.

 

GenericXmlApplicationContext를 사용하면서 parameter로 xml 파일을 넘기면 된다.

사용할 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 = "customerService" class="hello.core.customer.CustomerServiceImpl">
        <constructor-arg name="customerRepository" ref="customerRepository"/>
    </bean>
    
    <bean id = "customerRepository" class="hello.core.customer.MemoryCustomerRepository"/>
    
    <bean id = "orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="customerRepository" ref="customerRepository"/>
        <constructor-arg name="discountPolicy" ref="discountPolicy"/>
    </bean>
    
    <bean id="discountPolicy" class="hello.core.Discount.RateDiscountPolicy"/>
</beans>

보면 AppConfig 클래스와 비슷한 것을 볼 수 있다.

 

이 xml 파일을

package hello.core.xml;

import hello.core.customer.CustomerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class XmlAppContext {

    @Test
    void xmlAppContext(){
        ApplicationContext applicationContext = new GenericXmlApplicationContext("appConfig.xml");

        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Assertions.assertInstanceOf(CustomerService.class, customerService);
    }
}

이렇게 GenericXmlApplicationContext()로 넘겨주면 저번과 같이 동일하게 사용할 수 있다.

 

이렇게 다양한 형식을 지원하는 방법은 BeanDefinition 인터페이스를 사용하기 때문인데

xml을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만든다.

스프링 컨테이너는 어떻게 읽어왔는지 몰라도 BeanDefinition만 알면 된다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03
728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저번에 자바 코드만을 이용해 주문하는 프로그램을 만들어보았다.

 

기존 프로그램은 할인 정책이 고정적인 금액을 할인해 주는 방법이었는데, 이 방법에서 일정 비율로 할인을 해주는 방법으로 변경을 해본다고 하자.

 

그러면 discount패키지에

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;

public class RateDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountPercent = 5; // 할인되는 비율
    private int VVIPDiscountPercent = 10; //할인되는 비율

    @Override
    public int discount(Customer customer, int price) {
        if(customer.getGrade() == Grade.VIP){
            return price * VIPDiscountPercent / 100;
        }
        else if (customer.getGrade() == Grade.VVIP){
            return price * VVIPDiscountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

이런 코드가 추가되게 된다.

 

그러고 이 RateDiscountPolicy로 정책을 변경하려면

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import hello.core.customer.MemoryCustomerRepository;

public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository = new MemoryCustomerRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }
}

이렇게 OrderServiceImpl class의 코드를 변경해야 한다.

하지만 여기서의 문제는 분명 우리는 할인정책을 변경했지만 관련이 없는 Order 부분의 코드를 수정해야 한다.

지금 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 주기에 지금은 OCP를 위반 중이다.

 

 

현재 클래스 다이어그램을 보면 OrderServiceImpl은 FixDiscoubtPolicy도 의존하고 있고 이것을 변경하려면 FixDiscountPolicy에서 RateDiscountPolicy로 의존관계를 변경하는 것이다.

우리는 이런 그림에서 OrderServiceImpl이 완전히 DiscountPolicy에만 의존할 수 있도록 변경해야 한다.

이렇게 만들기 위해서는 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

 

우리는 관심사를 분리해주어야 한다.

하나의 클래스는 하나의 임무만을 가져야하고 지금 OrderServiceImpl은 본인의 임무도 수행을 하며 DiscountPolicy에 해당하는 클래스도 찾고 있는 것이다.

이 프로그램의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 관리하는 책임을 가지는 별도의 설정 클래스를 만들어야 한다.

package hello.core;

import hello.core.Discount.FixDiscountPolicy;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {
    
    public CustomerService customerService(){
        return new CustomerServiceImpl(new MemoryCustomerRepository());
    }
    
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryCustomerRepository(), new FixDiscountPolicy());
    }
}

이렇게 AppConfig 객체를 만들고 각 클래스에 들어가서 생성자를 통해 private final 객체를 입력받도록 만든다.

package hello.core.customer;

public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }
}

그렇게 해서 만든 CustomerServiceImpl을 잠깐 보면 CustomerRepository만 의존을 하고 MemoryCustomerRepository에 대한 내용은 나오지 않는 것을 볼 수 있다.

이제 CustomerServiceImlp은 의존관계에 대한 고민은 하지 않고 맡은 임무에만 집중을 하면 된다.

 

이에 대한 내용은 OrderServiceImlp도 마찬가지이다.

이렇게 의존관계를 외부에서 주입해주는 것 같다고 해서 우리말로 의존성 주입(Dependency Injection)이라고 한다.

 

이제 AppConfig에 맞게 App 클래스들에서 코드를 수정해 주자.

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;

public class CustomerApp {

    public static void main(String[] args) {
        
        AppConfig appConfig = new AppConfig();
        
        CustomerService customerService = appConfig.customerService();

        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        customerService.join(customer);

        Customer findCustomer = customerService.findCustomer(1L);
        System.out.println("new customer = " + customer.getName());
        System.out.println("find customer = " + findCustomer.getName());
    }
}

 

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

    public static void main(String[] args) {
        
        AppConfig appConfig = new AppConfig();
        
        CustomerService customerService = appConfig.customerService();
        OrderService orderService = appConfig.orderService();
        

        Long id = 1L;
        Customer customer = new Customer(id, "seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);

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

import 된 코드들을 보면 interface들에만 의존하는 것을 볼 수 있다.

 

테스트 코드들도 AppConfig에 맞게 수정을 하고

package hello.core.customer;

import hello.core.AppConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class CustomerServiceTest {

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

    @Test
    void join(){
        //given
        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        //when
        customerService.join(customer);
        Customer findCustomer = customerService.findCustomer(1L);

        //then
        Assertions.assertEquals(customer, findCustomer);
    }
}
package hello.core.order;

import hello.core.AppConfig;
import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    CustomerService customerService;
    OrderService orderService;

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

    @Test
    void createOrder(){
        Long id = 1L;
        Customer customer = new Customer(id, "Seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);
        Assertions.assertEquals(500, order.getDiscountPrice());
    }
}

테스트를 모두 실행해 보면

이렇게 문제없이 잘 수행되는 것을 볼 수 있다.

 

추가적으로 AppConfig에 가서 중복된 부분을 정리하도록 하자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }
    

    public OrderService orderService(){
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }
    
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }
    
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}

만약 여기서 할인 정책을 변경하려고 하면

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


    public OrderService orderService(){
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

이렇게 AppConfig의 코드만 수정을 하면 된다.

 

좋은 객체 지향 설계는 SOLID를 준수한다고 했다.

여기서 우리는

SRP

현재 하나의 클래스는 단 하나의 책임만 가지고 있다.

DIP

의존성 주입을 사용했기에 interface에 의존을 하고 있다.

OCP

요소를 확장하더라도 AppConfig만 수정을 하고 클라이언트들의 코드는 변경할 필요가 없다.

 

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.

하지만 AppConfig를 만든 이후에 구현 객체는 자신의 로직만을 실행하게 된다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간 것이다. OrderServiceImpl과 같은 클래스는 자신이 어떤 객체를 실행할지도 모른다.

이렇게 프로그램의 제어 흐름을 직업 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전 (Inversion of Control)이라고 한다.

 

애플리케이션 런타임 중에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.

의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

그리고 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 DI 컨테이너라고 한다.

 

이번에는 spring을 사용하여 DI를 적용해보자.

당연히 AppConfig 먼저이다.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


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

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

이렇게 AppConfig에 @Configuration을 붙여주고 각 메서드에 @Bean을 붙여준다. 이렇게 하면 스프링 빈이 등록이 된다.

 

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomerApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();

        //CustomerService customerService = appConfig.customerService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        customerService.join(customer);

        Customer findCustomer = customerService.findCustomer(1L);
        System.out.println("new customer = " + customer.getName());
        System.out.println("find customer = " + findCustomer.getName());
    }
}

그리고 기존에 AppConfig에서 꺼내오던 것을 ApplicationContext에서 꺼내오며, 각 의존관계들은 getBean을 이용해 받아온다.

 

OrderApp도

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();

        //CustomerService customerService = appConfig.customerService();
        //OrderService orderService = appConfig.orderService();

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


        Long id = 1L;
        Customer customer = new Customer(id, "seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);

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

이렇게 수정을 해준다.

그러면 전과 동일하게 동작하는 것을 볼 수 있다.

 

그리고 ApplicationContext를 스프링 컨테이너라고 한다.

저번에는 AppConfig를 직접 사용했지만, 이제부터는 스프링 컨테이너를 사용하면 된다.

 

스프링 컨테이너에서는 @Configuration이 붙은 클래스를 구성정보로 사용한다.

@Configuration이 붙은 클래스에서 @Bean이 붙은 메서드들을 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03
스프링 3일차  (0) 2023.02.01
728x90

한글을 사용할 경우에 깨지지 않고 정상적으로 출력하는 방법에 대해 알아보자.

 

한글을 이용해서 post 방식으로 넘긴다면

이런 식으로 깨져서 넘어가고 나타나게 된다.

 

이럴 땐

request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");

국제 표준인 UTF-8로 설정을 하면 된다.

 

일단 이름이랑 사는 곳을 입력 받을 jsp를 만들어보자.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<form action="mSignUp" method="post">
    이름 : <input type = "text" name="m_name"><br>
    사는 곳 : <input type="text" name="m_location">
    <input type="submit" value="sign up">
</form>

</body>
</html>

post 방식으로 mSignUp에 mapping 된 곳으로 넘긴다.

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/mSignUp")
public class SignUp extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        PrintWriter out = response.getWriter();

        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        String mName = request.getParameter("m_name");
        String mLoc = request.getParameter("m_location");

        out.print("<p> mName : " + mName + "</p>");
        out.print("<p> mLoc : " + mLoc + "</p>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

이렇게 mapping 된 Servlet에

request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");

 

을 추가하고 실행해보자.

아까와 다르게 한글이 제대로 표시되는 것을 볼 수 있다.

 

이번엔 Servlet이 아닌 jsp로 넘기는 경우를 살펴보자.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<form action="mSignUp.jsp" method="post">
    이름 : <input type = "text" name="m_name"><br>
    사는 곳 : <input type="text" name="m_location">
    <input type="submit" value="sign up">
</form>

</body>
</html>

action을 mSignUp.jsp로 바꾸어 주고

<% request.setCharacterEncoding("UTF-8");%>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<%!
    String mName;
    String mLoc;
%>

<%
    mName = request.getParameter("m_name");
    mLoc = request.getParameter("m_location");
%>

이름 : <%= mName%> <br>
사는 곳: <%=mLoc%>

</body>
</html>

이렇게 jsp 파일을 만들어준다.

 

그러면 이렇게 한글이 제대로 출력 되는  것을 볼 수 있다.

 

만약

<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%!
    String mName;
    String mLoc;
%>

<%
    mName = request.getParameter("m_name");
    mLoc = request.getParameter("m_location");
%>

이름 : <%= mName%> <br>
사는 곳: <%=mLoc%>

</body>
</html>

이런 jsp 파일을 사용했다면

 

이렇게 깨져서 나오는 것을 볼 수 있다.

 

이렇게 하나씩 코드를 추가하는 것 말고 filter를 사용해서 한글 출력을 할 수 있는데

filter interface를 구현해서 web.xml에 추가하는 것이다.

 

import jakarta.servlet.*;

import java.io.IOException;

public class myFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("-- filter doFilter() --");

        //request filter
        servletRequest.setCharacterEncoding("UTF-8");
        servletResponse.setCharacterEncoding("UTF-8");

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("-- filter init() --");
    }

    @Override
    public void destroy() {
        System.out.println("-- filter destroy() --");
    }
}

filter를 implements 해서 doFilter 안에 UTF-8로 인코딩 하는 코드를 달아준다.

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <filter>
        <filter-name>myFilter</filter-name>
        <filter-class>myFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>myFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

그리고 xml에 이렇게 filter 관련된 태그를 달아주면 위처럼 한글이 깨지지 않게 된다.

 

'백엔드 > JSP, Servlet' 카테고리의 다른 글

JSP & Servlet 11일차  (0) 2023.02.05
JSP & Servlet 10일차  (0) 2023.01.16
JSP & Servlet 9일차  (0) 2023.01.15
JSP & Servlet 8일차  (0) 2023.01.04
JSP & Servlet 7일차  (0) 2023.01.04
728x90

클라이언트와 서버의 연결을 유지시켜주는 방법으로 Session에 대해 알아보자.

 

쿠키는 연결 정보를 클라이언트에 저장을 했지만 Session은 서버에 저장하는 방법이다.

Session을 구현하는 방법은

다음과 같다.

 

바로 만들어보도록 하자.

우선 login.jsp이다.

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML>
<html>
<head>
    <title>Title</title>
</head>
<body>

<form action="loginCon" method="post">
    ID : <input type="text" name="mID"><br>
    PW : <input type="password" name="mPW"><br>
    <input type="submit" value="login">
</form>

</body>
</html>

이렇게 body 부분에 form으로 ID와 PW를 받고 post로 loginCon에 넘겨준다.

 

여기에 mapping된 메서드는

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/loginCon")
public class loginCon extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();

        String mID = request.getParameter("mID");
        String mPW = request.getParameter("mPW");

        out.print("mID : " + mID);
        out.print("mPW : " + mPW);

        HttpSession session = request.getSession();
        session.setAttribute("memberId", mID);

        response.sendRedirect("loginOk.jsp");

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}

어차피 doPost에서 doGet으로 넘어가기에 doGet에 저렇게 작성을 하고 request에서 mID와 mPW를 각각 받아온다.

그리고 session에 memberId로 mID 속성을 저장하고 loginOk.jsp를 열어준다.

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

    <%
        PrintWriter printWriter = response.getWriter();
        session = request.getSession();
        printWriter.print("memberId : " + session.getAttribute("memberId") + "<br>");
    %>

    <form action = "logoutCon" method="post">
        <input type = "submit" value="logout">
    </form>

</body>
</html>

 

loginOk.jsp에서는 session에서 memberId를 찾아 출력해주고 logoutCon을 연결해주는 버튼을 달아준다.

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;

import java.io.IOException;

@WebServlet(name = "logoutCon", value = "/logoutCon")
public class logoutCon extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        session.invalidate();

        response.sendRedirect("login.jsp");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

logoutCon은 session을 비워주고 처음화면인 login.jsp로 돌려 보내준다.

 

login.jsp는 session에 저장이 되어 있는지 확인을 해보아야 한다.

그렇기에 앞부분에 조건문을 추가하여 session에 데이터가 있다면 loginOk.jsp를 열어주게 만든다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    if(session.getAttribute("memberId") != null)
        response.sendRedirect("loginOk.jsp");
%>
<form action="loginCon" method="post">
    ID : <input type="text" name="mID"><br>
    PW : <input type="password" name="mPW"><br>
    <input type="submit" value="login">
</form>

</body>
</html>

'백엔드 > JSP, Servlet' 카테고리의 다른 글

JSP & Servlet 12일차  (0) 2023.02.05
JSP & Servlet 10일차  (0) 2023.01.16
JSP & Servlet 9일차  (0) 2023.01.15
JSP & Servlet 8일차  (0) 2023.01.04
JSP & Servlet 7일차  (0) 2023.01.04
728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

스프링 프레임워크는 최근에 스프링을 편리하게 사용할 수 있도록 지원해주는 스프링 부트의 등장으로 더 쉽게 사용할 수 있게 되었고 요즘은 이 스프링 부트를 사용하는 것이 기본이 되었다.

 

스프링은 객체 지향 언어를 따라 개발할 수 있도록 도와주는 프레임 워크이다.

 

여기서 나오는 객체 지향 언어를 따른 다는 것은 좋은 객체 지향 프로그램을 만든 다는 것인데

 

일단 객체 지향 언어의 특징으로는 

추상화

캡슐화

상속

다형성

이 있고 이것들은 자바의 기초만 배워도 알 수 있는 내용들이다.

 

이 특징들을 이용한 좋은 객체 지향 설계의 5가지 원칙이 있다.

줄여서 SOLID라고 하고

SRP: 단일 책임 원칙(single responsibility principle)

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

OCP: 개방-폐쇄 원칙(Open/Closed principle)

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

LSP: 리스코프 치환 원칙(Liskov subsititution principle)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

ISP: 인터페이스 분리 원칙(Interface segregation principle)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

DIP: 의존관계 역전 원칙(Dependency inversion principle)

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

가 있다.

 

이 원칙들에 의해 한 번 프로그램을 만들어보자.

start.spring.io에서 Dependencies를 모두 제외하고 생성을 해주었다.

intellij로 프로젝트를 열고 우리가 만들 프로그램의 요구사항을 정리하고 설계하자.

 

비즈니스 요구사항으로는

회원

회원을 가입하고 조회할 수 있다.

등급은 일반과 VIP, VVIP로 3가지 등급이 있다.

회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.

주문과 할인 정책

회원은 상품을 주문할 수 있다.

회원 등급에 따라 할인 정책이 다르게 적용이 된다.

VIP는 500원, VVIP는 1000원을 할인해주는 고정 금액 할인이 적용된다.

할인 정책은 변경될 가능성이 높다.

 

할인 정책은 인터페이스로 구현을 하고 언제든지 갈아끼울 수 있도록 설계하면 된다.

 

고객 도메인 관계

 

클래스 다이어그램

 

고객 등급은 BASIC, VIP, VVIP 3가지가 있기에 Customer 패키지에 enum으로 만들어준다.

package hello.core.customer;

public enum Grade {
    BASIC,
    VIP,
    VVIP
}

 

고객 class를 만들자.

package hello.core.customer;

public class Customer {

    private Long id;
    private String name;
    private Grade grade;

    public Customer(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;
    }
}

고객의 멤버에는 id, name, grade가 있을 것이며 생성자를 통해 넣어주고 Getter와 Setter를 만들어준다.

고객 객체에 대해 만들었으니 이제 이 객체를 저장할 저장소에 관하여 만들어보자.

 

아직 저장소가 선정되지 않았으니 인터페이스로 구현을 하고 Memory에 간단하게 저장할 수 있게 만든다.

package hello.core.customer;

public interface CustomerRepository {
    void save(Customer customer);

    Customer findById(Long id);
}

간단하게 이 인터페이스를 상속받아 메모리에 저장하는 리포지토리를 만들어둔다.

 

package hello.core.customer;

import java.util.HashMap;
import java.util.Map;

public class MemoryCustomerRepository implements CustomerRepository{

    private static Map<Long, Customer> store = new HashMap<>();

    @Override
    public void save(Customer customer) {
        store.put(customer.getId(), customer);
    }

    @Override
    public Customer findById(Long id) {
        return store.get(id);
    }
}

 

이 저장소를 사용하는 회원 서비스이다.

마찬가지로 인터페이스를 만들고 해당 인터페이스를 상속 받도록 만든다.

package hello.core.customer;

public interface CustomerService {
    void join(Customer customer);

    Customer findCustomer(Long id);
}

 

package hello.core.customer;

public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository = new MemoryCustomerRepository();

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }
}

 

일단 이렇게 회원 가입은 만들었으니, Junit을 통해 테스트를 해보자.

package hello.core.customer;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class CustomerServiceTest {

    CustomerService customerService = new CustomerServiceImpl();

    @Test
    void join(){
        //given
        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        //when
        customerService.join(customer);
        Customer findCustomer = customerService.findCustomer(1L);

        //then
        Assertions.assertEquals(customer, findCustomer);
    }
}

이렇게 테스트를 작성하고 Run 해보면

녹색불이 정상적으로 들어오는 것을 볼 수 있다.

 

회원가입은 구현을 했으니 이번에는 주문에 관련한 설계이다.

주문 도메인이다.

당연히 각 역할은 인터페이스로 먼저 구현을 할 예정이다.

 

그렇게 만든 클래스 다이어그램이다.

할인 정책에 관한 인터페이스이다.

package hello.core.Discount;

import hello.core.customer.Customer;

public interface DiscountPolicy {

    //return 이 할인 대상 금액
    int discount(Customer customer, int price);
}

 

일단 1000원, 500원으로 정해진 금액만 할인해 주고 있기 때문에

FixDiscountPolicy를 만든다.

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;

public class FixDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountFixAmount = 500; // 할인되는 금액
    private int VVIPDiscountFixAmount = 1000; //할인되는 금액

    @Override
    public int discount(Customer customer, int price){
        if(customer.getGrade() == Grade.VVIP){
            return VVIPDiscountFixAmount;
        }
        else if(customer.getGrade() == Grade.VIP){
            return VIPDiscountFixAmount;
        }
        else{
            return 0;
        }
    }
}

 할인 정책에 관한 부분을 모두 만들었다.

 

이번엔 주문에 관련된 부분이다.

일단 주문 결과를 주문 class로 넘겨주어야 하기 때문에 주문 class를 만든다.

주문 클래스의 멤버로는 주문한 고객의 id, 주문한 상품의 이름, 가격, 할인된 가격이 포함된다.

package hello.core.order;

public class Order {

    private Long id;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long id, String itemName, int itemPrice, int discountPrice) {
        this.id = id;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    public Long getId() {
        return id;
    }

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

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

이제 주문을 만들어주는 주문과 관련된 인터페이스와 그 인터페이스를 상속받는 class이다.

package hello.core.order;

public interface OrderService {

    Order createOrder(Long id, String itemName, int itemPrice);

}
package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import hello.core.customer.MemoryCustomerRepository;

public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository = new MemoryCustomerRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }
}

 

당연히 모두 만들었으면 Test를 진행해본다.

package hello.core.order;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.Grade;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    CustomerService customerService = new CustomerServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder(){
        Long id = 1L;
        Customer customer = new Customer(id, "Seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);
        Assertions.assertEquals(500, order.getDiscountPrice());
    }
}

 이렇게 VIP등급에 해당하는 고객은 500원이 할인되어야 하고 

하지만 이렇게 만든 프로그램은 좋은 객체 지향 프로그램이라고 할 수 없다.

그렇기에 다음 시간에 좋은 객체 지향 프로그램으로 바꾸어 보도록 하자.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 4일차  (0) 2023.02.03
스프링 3일차  (0) 2023.02.01
스프링 2일차  (0) 2023.01.18
728x90

스프링에서 DB에 접근하는 방법에 대해 공부해보자.

 

DB는 H2를 사용하며 다운로드를 받고 실행 한 후 테이블을 생성한다.

drop table if exists member CASCADE;
create table student
(
        id   bigint generated by default as identity,
        name varchar(255),
        primary key (id)
);

이렇게 student 테이블을 생성을 한다.

 

이제 스프링으로 돌아가서 jdbc에 대한 설정을 해준다.

build.gradle에 dependencies에서

implementation 'org.springframework.boot:spring-boot-start-jdbc'
runtimeOnly 'com.h2database:h2'

를 추가해 jdbc, h2 데이터베이스 관련 라이브러리를 추가한다.

 

동기화를 해주고

resources/application.properties에서

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasoure.username=sa

를 추가해준다.

동기화가 완료되면 빨간 줄이 드지 않을거다.

 

Jdbc 리포지토리 구현

Jdbc로 리포지토리를 구현해보려 했으나...

너무 어렵고 요즘은 쓰지 않기 때문에 여기에 코드를 적지는 않고 실행과 테스트만 해보도록 하겠다.

 

Service에서 사용하던 리포지토리를 memory에서 jdbc로 옮긴다는 것만 알고 있으면...

스프링을 껐다가 켜도 DB가 살아있다면 데이터가 유지되는 것을 볼 수 있다.

 

바로 테스트를 작성해보자.

package spring_practice.spring_practice.service;

import org.junit.jupiter.api.Assertions;
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 spring_practice.spring_practice.domain.Member;
import spring_practice.spring_practice.repository.MemberRepository;

@SpringBootTest
@Transactional
class MemberServiceDBTest {

    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;

    @Test
    public void join() throws Exception{
        //given
        Member member = new Member();
        member.setName("seungkyu");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberRepository.findById(saveId).get();
        Assertions.assertEquals(member.getName(), findMember.getName());
    }
}

DB를 사용할 때는 AfterEach를 사용하는 것이 아니라 @Transactional을 사용하면 테스트 후에 DB를 테스트 전으로 돌려준다.

 

AOP

모든 메서드에서 핵심은 아니지만 공통적으로 들어가는 사항이 있다. ex) 시간 측정

 

이 로직을 메서드 안에 넣어버리면 핵심 비즈니스의 로직과 섞여서 유지보수가 어려워 공통 관심 사항과 핵심 관심 사항으로 분리한다.

package spring_practice.spring_practice.aop;

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

@Aspect
@Component
public class TimeTraceAop {

    @Around("execution(* spring_practice.spring_practice..*(..))")
    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");
        }
    }
}

'백엔드 > 스프링' 카테고리의 다른 글

스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
스프링 3일차  (0) 2023.02.01
스프링 2일차  (0) 2023.01.18
728x90

의존관계

 

스프링 빈을 등록하고 의존관계를 설정하는 방법을 알아보자.

 

@Componenet를 사용하여 컴포넌트 스캔으로 자동 의존관계를 설정하는 방법과

자바 코드로 직접 스프링 빈을 등록하는 방법이 있다.

 

이제 컨트롤러를 사용하여 페이지를 보여주어야 하고 그 컨트롤러는 학생 서비스 객체를 이용해야 한다.

이 상황에서 컨트롤러에 서비스를 주입을 해주어야 하는데 그 과정을 살펴보자.

 

우선 컨트롤러를 만들어보자

이렇게 StuController가 생성이 될 때 @Autowired를 이용해 의존관계를 주입해주었다.

생성자에 @Autowired가 있으면 연관된 객체를 spring이 찾아서 넣어준다.

 

하지만 저렇게 빨간줄이 뜨는 것을 볼 수 있는데, spring이 실행될 때 memberService가 없기 때문이다.

따라서 이럴 때에는 의존되는 memberService도 스프링 빈으로 등록을 해주어야 한다.

(@Controller가 있으면 spring에 자동으로 등록이 된다.)

 

서비스에도 이렇게 @Service annotation을 달아준다.

여기에도 의존관계를 주입해주어야 하는 부분에서 등록이 되어 있지 않기 때문에 빨간 줄이 뜨는 것을 볼 수 있다.

여기도 다시 리포지토리를 찾아서 @Repository annotation을 달아주자.

 

@Controller, @Service, @Repository에 해당하는 annotation들은 @Component 를 포함하기 때문에 등록이 된다.

리포지토리까지 annotation을 달아주면

 

memberController -> memberService -> memberRepository 로 스프링에 의존관계가 설정이 된다.

 

직접 자바코드를 사용하여 등록을 하려면

 

@Bean annotation을 사용하여 Configuration을 해주는 객체를 생성해주면 된다.

 

웹 MVC 개발

 

이제 웹 페이지를 만들어보자

우선 관련 controller를 만든다

 

기본 페이지 관련 Controller와 html

package spring_practice.spring_practice.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";
    }
}

localhost에 들어가면 바로 home.html로 보내준다.

 

home.html이다.

<html xmlns:th="http://thymeleaf.org">
<head>
  <title>학생 관리</title>
</head>
<body>
<div class="container">
  <div>
    <h1>학생 관리 프로그램</h1>
    <p>회원 기능</p>
    <p>
      <a href="/student/new">신입생 등록</a>
      <a href="/student">학생 리스트</a>
    </p>
  </div>
</div>
</body>
</html>

 

신입생 등록 컨트롤러이다.

package spring_practice.spring_practice.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import spring_practice.spring_practice.service.MemberService;

@Controller
public class StuController {

    private final MemberService memberService;

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

    @GetMapping("/student/new")
    public String createForm(){
        return "student/createStuForm";
    }
}

 

만들었던 Controller에 @GetMapping annotation으로 해당 주소에 접근하면 student/createStuForm.html을 열어주는 메서드를 생성하고

<!DOCTYPE HTML>
<body>
<div class="container">
  <form action="/student/new" method="post">
    <div class = "form-group">
      <label form="name">이름</label>
      <input type="text" id="name" name="name" placeholder="이름을 입력하세요">
    </div>
    <button type="submit">신입생 등록</button>
  </form>
</div>
</body>

 

그리고 Controller와 같은 패키지에

package spring_practice.spring_practice.controller;

public class StuForm {
    private String name;

    public String getName() {
        return name;
    }

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

폼 관련 객체를 생성을 해준다.

 

이제 이 폼을 사용하여 등록하는 기능을 구현하자, @PostMapping을 사용하여 해당 페이지에서 작동하게 만들고

return redirect:/으로 home으로 보내준다.

package spring_practice.spring_practice.controller;

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.PostMapping;
import spring_practice.spring_practice.domain.Member;
import spring_practice.spring_practice.service.MemberService;

@Controller
public class StuController {

    private final MemberService memberService;

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

    @GetMapping("/student/new")
    public String createForm(){
        return "student/createStuForm";
    }

    @PostMapping("/student/new")
    public String create(StuForm form){
        Member member = new Member();
        member.setName(form.getName());
        memberService.join(member);

        return "redirect:/";
    }
}

이렇게만 하고 실행을 해보면

이런 home 페이지와

이런 신입생 등록페이지가 만들어지고 신입생 등록 버튼을 누르면 home 페이지로 이동하게 된다.

 

이제 조회기능을 추가하자, 컨트롤러에

    @GetMapping("student")
    public String list(Model model){
        List<Member> students = memberService.findMembers();
        model.addAttribute("students", students);
        return "student/studentList";
    }

이런 메서드를 추가하여 memberService에서 모든 학생 정보를 가져온 후 model의 속성에 추가해서 student/studentList.html로 넘겨준 후

 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
  <div>
    <table>
      <thead>
      <tr>
        <th>#</th>
        <th>name</th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="student : ${students}">
        <td th:text="${student.getStudentId()}"></td>
        <td th:text="${student.getName()}"></td>
      </tr>
      </tbody>
    </table>
  </div>
</div>
</body>
</html>

이렇게 each로 돌면서 모든 학생들을 보여지게 한다.

이러면

이렇게 페이지가 잘 보여지는 것을 확인 할 수 있다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03
스프링 2일차  (0) 2023.01.18

+ Recent posts