[Spring] 스프링 핵심원리 요약

wony·2024년 4월 30일

Spring

목록 보기
25/33

개요

내용: 미니프로젝트를 해보려고 하는데 중요한 부분들 위주로 요약해보려고 한다.
요약 : 애노테이션 정리 여기에 다 했다. 모르는거 있으면 계속 여기로 오자.

스프링 핵심 원리 이해1

1) 비즈니스 요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원은 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 있다.
public enum Grade {
	BASIC,
    VIP
}
  • enum 클래스는 보통 고정된 상수 집합을 나타내는 데 사용됩니다.
  • 여기서 Grade enum 클래스는 회원의 등급을 정의하기 위해 사용되고 있습니다.
  • 이는 BASICVIP 두 가지 등급만 존재하도록 강제할 수 있습니다.

enum 클래스 사용 이유

    1. 타입 안전성 제공
    • enum 을 사용하면 특정 값만 가질 수 있습니다. 이는 잘못된 값이 할당되는 것을 방지할 수 있습니다.
    1. 코드 가독성 향상
    • enum을 사용하면 코드가 더 읽기 쉬워집니다. 숫자나 문자열 대신 의미 있는 이름으로 등급을 나타낼 수 있습니다.
    1. 메서드 추가 기능
    • enum은 단순한 상수 집합 이상의 기능을 가질 수 있습니다. 각 상수에 관련된 메서드를 추가할 수 있습니다.

1. 관심사의 분리

1) AppConfig 등장

애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.

public class AppConfig{
	@Bean
    public MemberService memberService(){
    	return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService(){
    	return new OrderServiceImpl(
        	new MemoryMemberRepository(),
            new FixDiscountPolicy());
    }
}
  • @Bean 애노테이션
    • 역할 : @Bean 애노테이션은 메서드 수준에서 사용되며, 해당 메서드가 반환되는 객체를 스프링 컨테이너가 관리하는 빈으로 등록합니다.
    • 빈(Bean) 빈은 스프링 컨테이너가 생성하고 관리하는 객체입니다.
  • AppConfig 는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
    • MemberServiceImpl
    • MemoryMemberRepository
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig 는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.
    • MemberServiceImpl -> MemoryMemberRepository
    • OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy

MemberServiceImpl - 생성자 주입

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

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

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

    public Member findMember(Long memberId) {
        return memberRepository.findByID(memberId);
    }

}
  • 설계 변경으로 MemberServiceImplMemoryMemberRepository 를 의존하지 않는다.
  • 단지 MemberRepositoy 인터페이스만 의존한다.
  • 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
    클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI 의존관계 주입 또는 의존성 주입이라 한다.
  • OrderServiceImpl - 생성자 주입
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
	}

    @Override
	public Order createOrder(Long memberId, String itemName, int itemPrice) {
		Member member = memberRepository.findById(memberId);
		int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
	}
}
  • 설계 변경으로 OrderServiceImplFixDiscountPolicy를 의존하지 않는다.
  • 단지 DiscountPolicy 인터페이스만 의존한다.
  • OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.
  • OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig) 에서 결정한다.

AppConfig 실행

public class MemberApp {
	public static void main(String[] args) {
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();
		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 실행

public class OrderApp {

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

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

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

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

2) AppConfig 리팩토링

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

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

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

    public DiscountPolicy discountPolicy(){
        // return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • new MemoryMemberRepository() 이 부분이 중복 제거되었다. 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
  • AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다.
    애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
  • 만약 할인 정책을 변경한다면, 애플리케이션의 구성 역할을 담당하는 AppConfig 만 변경하면 된다.

3) 스프링으로 전환하기

AppConfig 스프링 기반으로 변경

@Configuration
public class AppConfig {

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

    @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();
    }
}
  • AppConfig에 설정을 구성한다는 뜻의 @Configuration을 붙여준다.
    각 메서드에 @Bean을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.
  • ApplicationContetxt를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

요약

  • @Configuration : 설정 클래스를 정의합니다. 이 클래스는 스프링 컨테이너가 관리하는 빈을 생성하고 구성하는 메서드를 포함합니다.
  • @Bean : 메서드 수준에서 사용되며, 해당 메서드가 반환하는 객체를 스프링 컨테이너에 빈으로 등록합니다.
  • 스프링 컨테이너는 @Configuration 이 붙은 클래스에서 @Bean 메서드를 호출하여 생성된 객체를 관리하고, 필요한 곳에 의존성을 주입합니다.

MemberApp에 스프링 컨테이너 적용

public class MemberApp{
	public static void main(String[] args) {

	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 도 이와 같이 적용
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
  • 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.

4) IoC, DI, 컨테이너

1. 제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
  • 반면에 AppConfig 가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig 가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.
  • 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라한다.

2. 의존관계 주입 DI(Dependency Injection)

  • OrderServiceImplDiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.

2. 스프링 컨테이너

5) 스프링 컨테이너와 스프링 빈

  • 스프링 컨테이너가 생성되는 과정을 알아보자
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라 한다.
  • ApplicationContext는 인터페이스이다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • 자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext) 를 만들어보자.
    • new AnnotationConfigApplicationContext(AppConfig.class);
    • 이 클래스는 ApplicationContext 인터페이스의 구현체이다.

6) 싱글톤 컨테이너

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig 는 요청을 할 때마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다!- > 메모리 낭비가 심하다.
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다 -> 싱글톤 패턴

1. 싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.

2. 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
  • 지금까지의 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

스프링 컨테이너를 사용하는 테스트 코드

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
	ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);

    //1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1 = ac.getBean("memberService",
MemberService.class);

    //2. 조회: 호출할 때 마다 같은 객체를 반환
	MemberService memberService2 = ac.getBean("memberService",
MemberService.class);

    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);

    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
}

  • 스프링 컨테이너 덕분에 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

3. 싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 고융하기 때문에 싱글톤 객체를 상태를 유지( stateful )하게 설계하면 안된다.
  • 무상태( stateless )로 설계해야 한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • @Bean 만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
    • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.

7) 컴포넌트 스캔 (ComponentScan)

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 등록하기 귀찮고 누락하는 문제도 발생한다.
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또 의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.

컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자

1. @Component

  • @Component@Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
    • 빈 이름 기본 전략: MemberServiceImpl 클래스 memberServiceImpl
    • 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면
    • @Component("memberService2") 이런식으로 이름을 부여하면 된다.

2. @Autowired 의존관계 자동 주입

  • 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
    • getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.

3. MVC 패턴

1) MVC 패턴

MVC 패턴의 등장
비즈니스 로직은 서블릿처럼 다른 곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면을 그리는 일에 집중할 수 있도록 MVC패턴이라는 것이 등장하였다.

MVC 패턴(Model View Controller) - 개요
MVC 패턴은 지금까지 학습한 것처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.

  • 컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달한 결과 데이터를 조회해서 모델에 담는다.
  • 모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
  • 뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.

MVC 패턴 - 적용

  • 서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용할 수 있다.
  • Model은 HttpServletRequest 객체를 사용한다. request는 내부에 데이터 저장소를 가지고 있는데, request.setAttribute(), request.getAttribute() 를 사용하면 데이터를 보관하고, 조회할 수 있다.

회원 등록
회원 등록 폼- 컨트롤러

@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
	String viewPath = "/WEB-INF/views/new-form.jsp";
	RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
	dispatcher.forward(request, response);
	}
}
  • dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.

redirect vs forward
리다이렉트는 실제 클라이언트에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.

회원 저장 컨트롤러와 회원 목록 조회 컨트롤러도 이와 같이 진행된다

MVC패턴 - 한계
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다. 특히 뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다.
하지만, View로 이동하는 코드가 항상 중복되고 사용하지 않는 코드도 존재하며, 공통 저리가 어렵다. 이를 위해 프론트 컨트롤러 패턴을 도입한다.

2) MVC 프레임워크 만들기

1. 프론트 컨트롤러 패턴 소개

  • 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하면 된다.프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.

2. View 분류

  • MyView 라는 클래스를 만들어 단순 반복 되는 뷰 로직을 분리하여 만든다.

4. 스프링 MVC - 시작하기

  • 이전에는 직접 MVC 패턴을 직접 만들어보며 적용해보았다.
  • 이제는 애노테이션 기반으로 MVC 패턴을 구현해보자.
  • 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다.

1) @RequestMapping

  • @RequestMapping
    • RequestMappingHandlerMapping
    • RequestMappingHandlerAdapter
  • RequestMapping 에서 찾고 Adapter 에서 처리

그럼 이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보자.

1. SpringMemberFormControllerV1 - 회원 등록 폼

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringMemberFormControllerV1 {

	@RequestMapping("/springmvc/v1/members/new-form")
	public ModelAndView process() {
	return new ModelAndView("new-form");
	}
}
  • @Controller:
    • 스프링이 자동으로 스프링 빈으로 등록한다. (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.)
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
  • @RequestMapping : 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
  • ModelandView : 모델과 뷰 정보를 담아서 반환하면 된다.

2. SpringMemberSaveControllerV1 - 회원 저장

@Controller
public class SpringMemberSaveControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
	String username = request.getParameter("username");
	int age = Integer.parseInt(request.getParameter("age"));

    Member member = new Member(username, age);
	System.out.println("member = " + member);
	memberRepository.save(member);

    ModelAndView mv = new ModelAndView("save-result");
	mv.addObject("member", member);
    return mv;
    }
}

3. SpringMemberListControllerV1 - 회원 목록

@Controller
public class SpringMemberListControllerV1 {
	private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members")
public ModelAndView process() {

    List<Member> members = memberRepository.findAll();

    ModelAndView mv = new ModelAndView("members");
	mv.addObject("members", members);
	return mv;
	}
}

2) 컨트롤러 통합

  • @RequestMapping 을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다. 따라서, 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.

3) 스프링 MVC - 실용적인 방식

  • 실무에서는 지금부터 설명하는 방식을 주로 사용한다. 로직을 전부 통합하여 사용
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }

    @PostMapping("/save")
    public String save(
            @RequestParam("username") String username,
            @RequestParam("age") int age,
            Model model) {
        Member member = new Member(username, age);
        memberRepository.save(member);
        model.addAttribute("member", member);
        return "save-result";
    }

    @GetMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        return "members";
    }
}
  • Model 파라미터
    • save() , members() 를 보면 Model을 파라미터로 받는 것을 확인할 수 있다.
  • ViewName 직접 반환
    • 뷰의 논리 이름을 반환할 수 있다.
  • @RequestParam 사용
    • 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.
    • @RequestParam("username")request.getParameter("username") 와 거의 같은 코드라 생각하면 된다. 물론 GET 쿼리 파라미터, POST Form 방식을 모두 지원한다.

4) 궁금한 점

1. @RequestMapping 은 어디에 사용해요?

  • @RequestMapping -> @GetMapping, @PostMapping
    • @RequestMapping 은 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 안된다.
    • @RequestMapping 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
    • 예를 들어서 URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리하면 된다.
    • @RequestMapping(value = "/new-form", method = RequestMethod.GET)
      이것을 @GetMapping , @PostMapping 으로 더 편리하게 사용할 수 있다.
      참고로 Get, Post, Put, Delete, Patch 모두 애노테이션이 준비되어 있다.

2. 아니 그래서 ComponentController는 뭐가 달라요?

  1. 용도와 역할:
    • @Component: 스프링 컴포넌트 스캔을 통해 해당 클래스를 스프링 빈으로 등록.
    • @Controller: Spring MVC에서 HTTP 요청을 처리하고 응답을 반환하는 컨트롤러 역할.
  1. @Component@Controller를 대체 가능한가?
    • 불가능합니다. @Controller@Component를 포함하며, 추가적인 요청 처리 기능을 제공합니다.
  1. Controller 클래스의 역할:
    • HTTP 요청과 응답 처리, URL 매핑, 서비스와 뷰 연결 등 클라이언트와 애플리케이션의 중간다리 역할.
  1. @Controller의 정체:
    • @Controller@Component를 메타 어노테이션으로 포함하며, 요청 매핑 및 Spring MVC 컨트롤러 역할을 수행하도록 설계.
  1. 결론:
    • @Component는 단순히 빈을 등록하지만, @Controller는 요청과 응답 처리를 위한 기능을 제공합니다. 따라서 @Controller는 단순한 @Component보다 더 많은 기능을 내포합니다.

5. Spring MVC 기본기능

1. 로깅

로깅 간단히 알아보기

앞으로 로그를 사용할 것이기 때문에, 이번시간에는 로그에 대해서 간단히 알아보자.

스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging )가 함께 포함된다.

스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
SLF4J - http://www.slf4j.org
Logback - http://logback.qos.ch

로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능
    • getClass() 는 현재 내 클래스의 로그를 출력한다는 의미

로그 호출
log.info("hello")
System.out.println("hello")

LogTestController

//@Slf4j
@RestController
public class LogTestController {
	private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
	public String logTest() {
		String name = "Spring";

        log.trace("trace log={}", name);
		log.debug("debug log={}", name);
		log.info(" info log={}", name);
		log.warn(" warn log={}", name);
		log.error("error log={}", name);

        //로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행,이런 방식 사용하면 X
		log.debug("String concat log=" + name);
		return "ok";
	}
}

매핑 정보

  • @RestController
    • @Controller 는 반환 값이 String 이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
    • @RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. 따라서, 실행 결과로 ok 메시지를 받을 수 있다.

테스트

  • 로그가 출력되는 포멧 확인
    • 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지
  • 로그 레벨 설정을 변경해서 출력 결과를 보자.
    • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
    • 개발 서버는 debug 출력
    • 운영 서버는 info 출력
  • @Slf4j 로 변경

로그 레벨 설정(application.properties)

#전체 로그 레벨 설정(기본 info)
logging.level.root=info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug

2. @PathVariable(경로 변수) 사용

1. PathVariable 사용 - 단일

/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
	log.info("mappingPath userId={}", data);
	return "ok";
}
  • 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.

  • /mapping/userA

  • /users/1

  • @RequestMapping 은 URI 경로를 템플릿화 할 수 있는데, @PathVariable을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.

2. PathVariable 사용 - 다중

/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
orderId) {
	log.info("mappingPath userId={}, orderId={}", userId, orderId);
	return "ok";
}

3. 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
	log.info("mappingConsumes");
	return "ok";
}

3. HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

  • 요약 : 1,2 번은 @ModelAttribute를 사용하자. 3번은 @RequestBody 사용

1. HTTP 요청 데이터 조회 - 개요
HTTP 요청을 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자.

    1. GET - 쿼리 파라미터
    • /url?username=hello&age=20
    • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
    • 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
    1. POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age = 20
    • 예) 회원 가입, 상품 주문, HTML Form 사용
    1. HTTP message body 에 데이터를 직접 담아서 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH

2. 요청 파라미터 - 쿼리 파라미터, HTML Form

HttpServletRequestrequestgetParameter() 를 사용하면 다음 두가지 요청 파라미터를 조회할 수 있다.

GET, 쿼리 파라미터 전송

예시 : `http://localhost:8080/request-param?username=hello&age=20`

POST, HTML Form 전송
예시

POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20

GET 쿼리 파라미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있다. 이것을 간단히 요청 파라미터( request parameter ) 조회라 한다.

2-1. HTTP 요청 파라미터 - @RequestParam

스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다.

requestParamV2

/**
* @RequestParam 사용
* - 파라미터 이름으로 바인딩
* @ResponseBody 추가
* - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
*/
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
	@RequestParam("username") String memberName,
	@RequestParam("age") int memberAge) {

    log.info("username={}, age={}", memberName, memberAge);
    return "ok";
}
  • @RequestParam : 파라미터 이름으로 바인딩
  • @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력

requestParamV3

/**
* @RequestParam 사용
* HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
	@RequestParam String username,
	@RequestParam int age) {
	log.info("username={}, age={}", username, age);
	return "ok";
}

requestParamV4

/**
* @RequestParam 사용
* String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
	log.info("username={}, age={}", username, age);
	return "ok";
}

2-2. HTTP 요청 파라미터 - @ModelAttribute(중요!)
실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 다음과 같이 코드를 작성한다.

@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
  • 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.

먼저 요청 파라미터를 바인딩 받을 객체를 만들자

import lombok.Data;
@Data
public class HelloData {
	private String username;
	private int age;
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
	log.info("username={}, age={}", helloData.getUsername(),
	helloData.getAge());
	return "ok";
}
  • HelloData 객체가 생성되고, 요청 파라미터의 값도 모두 들어가 있다.
  • 스프링 MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
    • HelloData 객체를 생성한다.
    • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다.
    • 예) 파라미터의 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.

3. HTTP 요청 메시지 -단순 텍스트

  • HTTP message body에 데이터를 직접 담아서 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH
  • 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우 @RequestParam, @ModelAttribute를 사용할 수 없다.
  • @RequestBody 를 사용하자

@RequestBody - requestBodyStringV4

  • @RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 참고로 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.
  • 이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam ,@ModelAttribute 와는 전혀 관계가 없다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
        log.info("messageBody={}", messageBody);
        return "ok";
}
  • 정리 : @RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다

HTTP 요청 메시지 - JSON

HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해보자.
이때도, 결국에는 @RequestBody를 사용해주면 된다.

requestBodyJsonV5

@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data){

        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }
}

@ResponseBody
응답의 경우에도 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다. 물론 이 경우에도 HttpEntity 를 사용해도 된다.

  • @RequestBody 요청
    • JSON 요청 -> HTTP 메시지 컨버터 -> 객체
  • @ResponseBody 응답
    • 객체 -> HTTP 메시지 컨버터 -> JSON 응답

총정리

  • @RequestParam@ModelAttribute HTTP 요청의 파라미터를 처리합니다.
    • @RequestParam은 HTML 폼 데이터나 쿼리 파라미터를 처리할 때 사용됩니다.
  • @RequestBody@ResponseBodyHTTP 요청과 응답의 본문(body)을 처리합니다.
    • @RequestBody@ResponseBody는 주로 JSON 또는 XML과 같은 데이터를 주고받을 때 사용됩니다.
  • @Controller vs @ResponseBody
    • @Controller 가 붙은 클래스의 메서드는 기본적으로 뷰(View)를 반환합니다. 즉, JSP, Thymeleaf 같은 템플릿 엔진을 통해 HTML 페이지를 생성하고, 이를 클라이언트에 전달합니다.
    • 이 애노테이션을 사용하면 메서드의 반환값이 뷰 리졸버( View Resolver )를 거치지 않고, 그대로 HTTP 응답으로 전달됩니다.
profile
안녕하세요. wony입니다.

0개의 댓글