Spring 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 정리 인프런 강의 정리 (1)

Spring 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 정리 인프런 강의 정리 (1)

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 정리

현업에서는 system.out.println을 사용하지 않고 log를 많이 사용한다. 보아서 볼 수 있기 때문.

JUnit은 테스트를 하는 도구로써 Java진영에서 많이 사용한다.

테스트 라이브러리

  • junit : 테스트 프레임워크
  • mockito : 목 라이브러리
  • assertj : 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
  • spring-test : 스프링 통합 테스트 지원

터미널에서 Spring을 실행하는법 (Mac에서 하는 방법)

실행하려는 프로젝트에서 터미널을 열고

./gradlew build
cd build/libs
java -jar hello-spring-0.0.1-SNAPSHOT.jar

을 하고 실행되는지 확인 해 보면 된다. 종료할땐 ^C로 종료하면 된다.

서버를 배포 할 때는 jar파일만 복사에서 서버에 넣어주고 java -jar로 실행만 시켜주면 된다.

gradlew clean을 하면 build 폴더를 지울 수 있다.

IntelliJ에서 cmd + shift + enter를 누르면 자동완성을 시켜준다.

정적 컨텐츠 : HTML 파일을 그대로 반환하는 것

MVC와 템플릿 엔진

MVC : Model View Controller

Model과 Controller는 비즈니스 로직을 담당하고 View는 화면을 그리는데에 집중해야한다.

API 방식 : 문자열을 반환하는 것이 아니라 객체를 반환하는 것

@ResponseBody를 사용하면 뷰 리졸버를 사용하지 않고 문자열을 반환하게 된다. (HTTP의 Body에 문자열을 반환. HTML Body태그를 말하는 것이 아니다.)

@ResponseBody 어노테이션이 붙었을 경우 return 값에 따라 다음과 같은 컨버터가 동작한다.

  • 단순 문자일 경우 : String Converter가 동작하여 문자열을 반환한다.
  • 객체일 경우 : Json Converter가 동작하여 JSON 형식으로 반환한다. 객체일 경우 Default로 JSON 형식으로 반환한다. xml을 원하면 설정으로 가능하다.

사실 실부에서 ResponseBody의 설정을 손을 데는 일은 거의 없다.

IntelliJ에서 Ctrl + Enter를 누르면 인터페이스의 구현체 오버라이드를 간편하게 할 수 있다.

클래스 가져올 땐 alt + shift + enter를 누르면 클래스를 가져올 수 있다.

테스트 할 때는 관례상 테스트 할 클래스의 이름 뒤에 Test를 붙여준다. 테스트 할 때는 굳이 public을 붙이지 않아도 된다.

테스트할 때는 테스트 할 메소드 위에 @Test 어노테이션을 붙여준다.

import org.assertj.core.api.Assertions;

Assertion.assertThat(테스트 대상 1).isEqualTo(테스트 대상 2);

위 방식이 테스트에서 제일 많이 사용하는 방법 중 하나다.

option + enter를 하면 static import를 할 수 있다.

변수명을 여러개 변경 하려는 경우에는 변경하려는 변수명을 클릭하고 shift + f6

@AfterEach는 해당 어노테이션이 붙은 메소드를 @Test 를 실행하고 끝날 때 마다 실행되게 한다.

cmd + option + v : 대입식 바로 만들어줌

Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

Optional에서 지원하기에 가능한 기능이다.

memberRepository.findByName(member.getName()).ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

이런 식으로 간략하게 하는 편이 좋다.

위와같이 findByName()안에 로직이 나열되어 있는 경우에는 메소드로 따로 뺴주는 편이 좋다.

ctrl + T를 누르면 메소드로 따로 분리한다던지 하는 리펙토링을 하는데 편하다

Service 쪽에 있는 클래스들은 비즈니스 로직에 가까운 이름으로 작명하여야 한다.

cmd + shift + T를 하면 해당 클래스의 테스트 케이스를 쉽게 만들 수 있다.

테스트 메소드의 이름은 한글이나 일본어를 사용할 수 도 있다. 실제로 현업에서는 한글을 많이 사용한다. 빌드 될 때 Test는 포함되 지 않기 떄문에 상관없다.

테스트를 할때는 given when then 이 3가지를 주석으로 두고 생각하면서 만들자!

테스트는 동작만 확인할 게 아니라 예외처리가 잘 되는지도 확인해야 한다.

@Test
    public void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        // when
        memberService.join(member1);
        try{
            memberService.join(member2);
            fail( );
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
    }

테스트에 굳이 try catch를 사용하지 않는 방법도 존재한다.


    @Test
    public void 중복_회원_예외() {
        // 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("이미 존재하는 회원입니다.");
    }

assertThrows를 사용하면 try catch를 사용하지 않아도 된다.

memberServiceTest() 전체코드

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        // given
        Member member = new Member();
        member.setName("spring");

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

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        // when
        memberService.join(member1);
//        try{
//            memberService.join(member2);
//            fail( );
//        } catch (IllegalStateException e) {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        // then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

위 코드에서 @BeforeEach 어노테이션은 실행되기 전에 돌아가는 것이다. 해당 어노테이션에 있는 메서들를 통해 memberService 클래스는 memberRepository 의존성 주입(DI)을 받게 된다.

스프링 빈은 SpringApplication이 있는 위치와 그 이하의 디렉토리에 있는 컴포넌트만 등록한다.

스프링 빈을 수동으로 등록할 수도 있다.

package hello.hellospring;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

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

@Configuration 어노테이션을 붙이고 @Bean 어노테이션을 써주면 스프링 빈에 등록이 가능하다.

의존성 주입 (DI) 에는 3가지 방식이 존재한다.

  1. 생성자 주입
  2. 필드 주입
  3. 세터 주입

3가지 방식중에서는 생성자 주입을 권장한다.

스프링에서 의존성 주입 방식 중 필드 주입과 세터 주입을 권장하지 않는 주된 이유는 다음과 같다

1. 필드 주입(Field Injection) 문제점

public class MemberService {
    @Autowired
    private MemberRepository memberRepository; // ❌
}
  • 불변성(Immutable) 보장 불가: 필드가 final로 선언될 수 없어 객체 생성 후 의존성 변경 가능 테스트 어려움: 순수 자바 테스트 시 Mock 객체 주입 불가능 (반드시 스프링 컨테이너 필요)

의존성 숨김: 생성자 없이 의존 관계 파악 어려움

2. 세터 주입(Setter Injection) 문제점

public class MemberService {
    private MemberRepository memberRepository;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) { // ❌
        this.memberRepository = memberRepository;
    }
}
  • 변경 가능성 노출: 런타임 중 의존성 변경 가능 (객체 불변성 파괴)

순환 의존성 발생 가능: 스프링 컨테이너 초기화 시 순환 참조 문제 발생 가능

Null 안정성 부족: 주입 전에 메서드 호출 시 NPE 발생 위험

✅ 생성자 주입 권장 이유

public class MemberService {
    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) { // ✅
        this.memberRepository = memberRepository;
    }
}

불변성 보장: final 키워드 사용 가능

  • 순환 의존성 사전 감지: 애플리케이션 구동 시점에 오류 발생
  • 테스트 용이성: new 연산자로 객체 생성 시 의존성 주입 간편
  • 코드 품질 향상: 생성자 파라미터 개수로 클래스 복잡도 가시화

📊 공식 문서 권장 사항

스프링 팀은 2016년부터 공식적으로 생성자 주입을 권장하고 있습니다. (Spring Framework 4.3 이후 단일 생성자 경우 @Autowired 생략 가능)

⚠️ 예외 사항

  • 옵셔널한 의존성(선택적 주입)이 필요한 경우에만 세터 주입 사용
  • 부트스트랩 클래스나 @Configuration 클래스에서의 제한적 필드 주입 허용

cmd + e 누르면 가장 최근에 봤던 파일 목록들을 볼 수 있다