hola 개발

[ Test ] 제대로 익히는 Spring Boot 테스트 방법 #1. 기본 본문

프레임워크/스프링

[ Test ] 제대로 익히는 Spring Boot 테스트 방법 #1. 기본

hola. 2025. 7. 7. 16:37

## 스프링 애플리케이션 테스트 코드는 글 마지막에 있습니다 ##

- 왜 써야하는가? 

 개발을 하면서 어떤 기술이나 방법론을 배울 때는 왜 써야하는지? 에 대한 질문이 중요한 것 같습니다. 왜냐하면 아무리 좋은 기술이라고 해도 특정 개발 환경에서는 굳이 필요 없을 수 있기 때문입니다. 굳이 필요 없는 기술을 도입하는 것은 곧 인력 낭비가 될 수도 있기 때문에 개발 환경에 맞는 기술을 선택하는 것도 중요하다고 생각합니다.

 

 현재 다니는 회사의 프로젝트에는 테스트 코드가 존재하지 않습니다. 테스트 코드가 없다고 크게 불편함을 느낀적은 없습니다. 앞서서 말했듯이 굳이 필요 없다고 생각했기에 그동안 이 회사를 다니면서 테스트 코드를 작성하지 않았습니다. 문제는 회사 프로젝트를 마이그레이션 담당하면서 생겼습니다. 기존에 잘 작동하던 코드들이 spring 버전을 바꾸면서 에러나기 시작한 것입니다. 코드 하나하나 서버를 다시 실행시키고 웹에서 기능을 클릭하여 테스트를 하면서 ' 아... 기존에 작성된 테스트 코드가 있었다면 문제를 더 빨리 파악할 수 있었지 않았을까? ' 하는 생각이 들었고 테스트 코드를 작성해야 겠다는 마음을 먹게 되어, 테스트 코드에 대한 공부를 시작하게 되었습니다.

 

- 테스트가 없다면?

1. 사소한 변경에도 시스템 전체가 깨질 수 있다.

단일 조건 변경이 도미노처럼 전체 기능에 영향을 주고, 원인 파악도 어려워질 수 있습니다.

 

2. 기능이 정상 동작하는지 일일이 수작업으로 확인해야 한다.

예를 들어 회원 가입 기능에서 이메일 인증 방식을 변경했다고 하면, 매번 브라우저 열고 회원 가입 클릭을 반복 테스트 해야합니다.

이러한 번거로운 작업은 시간을 소비하게 됩니다.

 

3. 리펙토링이나 개선이 어려워진다.

리펙토링 후 기존 기능이 깨졌는지 확인이 불가하게 된다.

또한 괜히 코드를 건드려서 안되는 것보다는 냅두는게 낫다고 판단하여 코드 개선을 못하게 되고 기술 부채가 누적된다.

(저는 서두에 말했듯이 이 부분에서 테스트 코드의 필요성을 절감하게 되었습니다!)

 

테스트가 없다면 위와 같은 문제가 발생할 수 있습니다.

그렇다면 Spring 앱에서 어떻게 테스트 코드를 작성해야 할까요?

Spring은 테스트를 지원하기 위해 Spring Test라는 이름으로 공식적인 테스트 지원 프레임워크를 제공합니다.

 

- Spring Test란?

Spring에서 테스트를 지원하기 위해 제공하는 모듈/기능의 집합입니다. spring-test 모듈에 포함되어 있으며, 단위 테스트, 통합 테스트, 슬라이스 테스트, 웹 API 테스트 등을 할 수 있습니다. 

또한 다양한 프레임워크와 통합해서 사용할 수 있습니다. 

예를 들어 JUint은 실행 플랫폼으로, Spring TestContext Framework는 실행 시 스프링 환경을 구성해주는 엔진으로 조합하여 테스를 실행할 수 있습니다.

이를 기반으로 다양한 테스트를 통해 스프링 환경 전체를 안정적으로 구성할 수 있도록 도와줍니다.

- Spring Test 의 핵심 기능

[ 컨텍스트 구성 ]

스프링 기반 테스트는 단순한 자바 테스트와 달리 ApplicationContext를 로딩해서 빈을 DI 받아 테스트합니다.

이때 불필요한 설정을 로딩하거나 잘못된 컨텍스트 구성을 쓰면 느려지고 오류가 납니다.

따라서 테스트에 맞게 테스트 시 어떤 설정 파일을 쓸지, 어떤 빈을 포함할지, 어떤 환경으로 실행할지를 지정하는 것입니다.

즉, 유연한 컨텍스를 구성하는 것이며, 특정 빈과 구성을 테스트에 맞춰서 로드하는 것입니다.

 

[ 테스트 슬라이싱 ]

Spring Boot에서 애플리케이션 전체를 띄우지 않고, 특정 계층(레이어)만 분리해서 테스트하는 방식입니다.

컨트롤러, 서비스, 레포지토리 등 특정 계층만 집중적으로 테스트하고 싶을 때 사용하며 단위 테스트와 유사합니다.

- 대표적인 테스트 유형

 저는 처음에 공부할 때 이 부분이 이해가 되지 않았습니다.

'테스트 유형? 그냥 기능이 잘 돌아가는지 확인 할 수 있도록  테스트 코드를 작성하면 되는 것 아닌가?' 

이 말은 맞습니다만 추상적입니다. 기능이 잘 돌아가는지 확인이라는 말이 추상적이라는 말입니다.

 코드로 구현하려면 추상적인 말을 구체적으로 풀어내야 합니다. 

기능이 잘 돌아가는지 확인이란 말을 구체적으로 표현하자면 리포지토리 메서드의 반환값 확인 테스트 , 컨트롤러 + 서비스 + 리포지토리 함께 통합해서 정상 작동하는지 라고 할 수 있습니다. 

 이렇게 구체적으로 테스트 할 영역을 구분한 것이 테스트 유형이라고 할 수 있습니다. 

 

[ 단위 테스트 ]

하나의 클래스(메서드)의 비즈니스 로직만 단독으로 테스트하는 것이며 스프링 컨테이너 없이 실행 가능합니다.

의존 객체는 목(Mocking) 처리하는 방법이 있습니다.

일반적으로 서비스 및 리포지토리 계층을 대상으로 다양한 시나리오에서 올바르게 작동하는지 확인합니다.

데이터베이스나 API와 같은 외부 시스템에 의존하지 않고 개별 구성 요소를 테스트하는 데 초점을 맞춥니다.

단위 테스트는 빠르게 실행되며 코드 변경 사항에 대한 즉각적인 피드백을 제공 받을 수 있습니다.

단위 테스트에 일반적으로 사용되는 프레임워크에는 JUnit과 Mockito가 있습니다.

 

[ 통합 테스트 ]

스프링 ApplicationContext를 로딩하여 여러 컴포넌트(예: Controller + Service + Repository)를 함께 통합하여 테스트하는 방식입니다. 이 때 실제 DB(H2 등) 또는 TestContainer 연동 가능합니다.

즉, 스프링에서 통합 테스트는 서비스 계층과 데이터 계층과 같은 다양한 계층 간의 상호작용을 검증하는 데 필수적입니다.

통합 테스트는 모듈이 예상대로 함께 작동하는지 확인하도록 설계합니다.

스프링은 Spring Test 프레임워크를 통해 통합 테스트를 쉽게 설정할 수 있도록 합니다. 

예를 들어 http 요청에 대한 응답값을 테스트 하는 것이 될 수 있습니다.

 

[ 엔드 투 엔드 테스트 (E2E) ]

사용자의 실제 사용 흐름을 전체 시스템 수준에서 테스트 하는 방식이며, API 호출, DB 저장, 화면 동작까지 포함합니다.

애플리케이션의 모든 구성 요소가 의도한 대로 함께 작동하는지 확인하는 테스트 입니다.

Selenium, Cypress 또는 스프링 자체 테스트 프레임워크와 같은 도구를 활용하여 E2E 테스트를 할 수 있습니다.

- 스프링의 테스트 프레임워크 종류

[ JUnit ]

JUnit은 자바 애플리케이션에 가장 널리 사용되는 단위 테스트 프레임워크로, 자동화된 테스트 작성을 쉽게 해주고 TDD 및 CI/CD 환경에서 필수적인 역할을 합니다.

테스트 케이스를 생성하고, 예상 결과를 확인하며, 테스트를 구성하는 간단하고 효과적인 방법을 제공합니다.

 

JUnit 5 주요 어노테이션 (Jupiter)

@Test 테스트 메서드 표시
@BeforeEach 각 테스트 메서드 실행 전 실행
@AfterEach 각 테스트 메서드 실행 후 실행
@BeforeAll 모든 테스트 시작 전 1회 실행 (static 필요)
@AfterAll 모든 테스트 종료 후 1회 실행 (static 필요)
@Disabled 해당 테스트를 비활성화
@Nested 중첩 테스트 클래스 정의
@DisplayName 테스트 이름을 명확히 지정
@Tag 테스트 분류를 위한 태그 지정
@ParameterizedTest 파라미터 기반 테스트 실행
@ValueSource, @CsvSource, @MethodSource 등 파라미터 값 제공 어노테이션

예시) 스프링 앱의 컨트롤러의 메서드를 JUnit으로만 단위 테스트

class MemberControllerTest {
    private MemberController memberController;

    @BeforeEach
    void setUp() {
        // 가짜 서비스 직접 구현
        MemberService fakeService = new MemberService(new FakeMemberRepository());
        memberController = new MemberController(fakeService);
    }

    @Test
    @DisplayName("MemberController join 테스트")
    void join() {
        Member member = new Member("test");

        ResponseEntity<?> response = memberController.join(member);

        // 회원 가입 성공시에 response 객체의 status 코드가 201로 같은지 확인
        assertEquals(HttpStatus.CREATED, response.getStatusCode());

        // 실제 response객체에 id가 있는지 확인
        String id = (String) ((Map<?, ?>) response.getBody()).get("id");
        assertNotNull(id);
    }

    /**
     * 내부 테스트용 Repository
     * */
    static class FakeMemberRepository implements MemberRepository {
        private final Map<String, Member> store = new HashMap<>();

        @Override
        public Member save(Member member) {
            store.put(member.getId(), member);
            return member;
        }

        @Override
        public Optional<Member> findById(String id) {
            return Optional.ofNullable(store.get(id));
        }

        @Override
        public Optional<Member> findByName(String name) {
            return store.values().stream()
                    .filter(m -> m.getName().equals(name))
                    .findFirst();
        }

        @Override
        public List<Member> findAll() {
            return new ArrayList<>(store.values());
        }

        @Override
        public void clear() {
            store.clear();
        }
    }

}

 

 

[ TestContext Framework ] 

 JUnit 테스트는 단순한 자바 객체 테스트만 가능합니다.

그러나 스프링 기반 앱을 테스트 할 때는 @Autowired 같은 의존성 주입, 트랜잭션 처리, @Configuration 설정 등이 필요한데, 이를 가능하게 해주는 것이 TestContext Framework입니다. TestContext Framework는 스프링 구성 요소의 통합 테스트를 지원하기 위해 구축되었습니다. 테스트 클래스에 빈을 주입할 수 있어서 풍부한 테스트 시나리오를 작성할 수 있게 도와줍니다.

 

TestContext Framework는 다음과 같은 특징이 있습니다.

1. 컨텍스트 캐싱 : 애플리케이션 컨텍스트를 캐싱하여 동일한 구성이 요청될 경우 재사용함으로써 테스트를 빠르게 수행합니다.

2. 트랜잭션 관리 : 트랜잭션 관리 지원을 제공하여 각 테스트 후 데이터베이스 변경 내용을 롤백할 수 있습니다.

3. 의존성 주입 : 테스트 클래스에 빈을 주입할 수 있어서 다양한 테스트 시나리오를 작성할 수 있습니다.

 

 JUnit 과 TestContext Framework을 조합하면 통합 테스트가 가능합니다.  

주요 구성 요소

TestContext 테스트에 필요한 Spring 컨텍스트 정보(설정 클래스, 빈, 테스트 인스턴스 등)를 관리
TestContextManager 테스트 실행 시 컨텍스트를 생성하고, 의존성 주입을 수행
TestExecutionListener 테스트 실행 전후에 다양한 작업을 삽입할 수 있는 훅 (예: 트랜잭션 처리, DI, 이벤트 등)
@ContextConfiguration 어떤 설정 클래스를 사용할지 정의
@TestExecutionListeners 실행 리스너들을 직접 정의 가능

 

아래와 같은 어노테이션이 TestContext Framework 기반입니다.

@SpringBootTest 전체 컨텍스트 로딩
@WebMvcTest, @DataJpaTest 등 슬라이스 테스트
@ContextConfiguration 테스트 컨텍스트 수동 설정
@TestPropertySource 외부 속성 파일 지정
@ActiveProfiles 테스트 시 적용할 스프링 프로파일 지정
@Transactional 테스트 실행 후 자동 롤백
@Autowired 테스트 클래스에 의존성 주입 가능

 

예시) JUnit과  TestContext Framework 조합한 통합테스트

@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MemberControllerIntegrationTest {
    @Autowired
    private MemberController memberController;

    @Autowired
    private MemberRepository memberRepository;

    @BeforeEach
    void clearRepo() {
        memberRepository.clear(); // MemoryMemberRepository는 clear 메서드 있음
    }

    @Test
    @DisplayName("회원가입 성공 후 리포지토리 검증")
    void join() {
        // given
        Member member = new Member("test");

        // when
        ResponseEntity<?> response = memberController.join(member);

        // then
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        String id = (String) ((Map<?, ?>) response.getBody()).get("id");
        assertNotNull(id);

        // 리포지토리에서 직접 확인
        Optional<Member> saved = memberRepository.findById(id);
        assertTrue(saved.isPresent());
        assertEquals("test",saved.get().getName());
    }
}

 

그런데! TestContext Framework는 어떻게 동작하기에 빈을 가져올 수 있는걸까요?

1. 테스트 클래스에 @SpringBootTest, @ContextConfiguration 등이 붙어 있으면

2. TestContextManager가 ApplicationContext 를 생성

이 ApplicationContext는 실제 앱과 동일하게 다음을 수행함:

 @ComponentScan, @Configuration, @Bean 해석 ,@Component, @Service, @Repository, @Controller 등 스프링 빈 등록3. 등록된 빈들이 생성되고 스프링이 관리하는 싱글톤 객체로 유지됨

4. 테스트 클래스의 @Autowired 필드에 이미 만들어진 빈이 주입

위와 같은 과정을 통해 테스트 클래스에서 빈을 사용할 수 있게 되는 것입니다.

 

[ Mockito ]

Mokito는 Java에서 널리 사용되는 목 객체를 생성하여 테스트 가능하게 해주는 프레임워크 입니다.  

예를 들어 service계층이나 cotroller계층을 테스트 시 Mockito를 활용해 repository나 service를 Mock 객체로 만들어서 테스트를 진행할 수 있습니다. 외후 호출 없이 메모리 내에서 동작하기 때문에 테스트가 빠르다는 장점이 있습니다.

이 때 중요한 점은 mock 객체는 빈 껍데이라는 것입니다.

다음은 mock객체를 활용해 member service의 회원 가입을 테스트하는 코드입니다.

class MemberServiceTest {

    private MemberRepository memberRepository;
    private MemberService memberService;

    @BeforeEach
    void setUp(){
        memberRepository = Mockito.mock(MemberRepository.class); // memberRepository Mock 인스턴스 생성
        memberService = new MemberService(memberRepository);
    }

    @Test
    @DisplayName("join 테스트 실패")
    void join() {
        // given
        Member test = new Member("test");

        // when
        String joinTestId = memberService.join(test);

        // then
        Optional<Member> testMember = memberRepository.findById(joinTestId);
        assertEquals(test.getId(),testMember.get().getId());
    }
}

member repository는 mock 객체로 구현되어서 meberService에 주입 됩니다.

memberService에서 join시에 mock으로 구현된 member repository에서는 join 시도할 것 같으나 그렇지 않다는 것입니다.

왜냐하면 mock으로 구현된 member repository 는 내부에는  MemberRepository 클래스에 있던 코드들이 구현되지 않은 빈 껍데기 일뿐이기 때문입니다. 

왜 그럴까요? 위에서 테스트 유형에서 말했듯이, Mock 객체는 단위 테스트를 위한 도구이기 때문입니다. 어떤 클래스의 내부 로직이 외부 의존성 없이 제대로 작동하는지를 테스트하기 위한 도구이기 때문입니다. 위의 테스트를 진행하기 위해서는 mock으로 구현된 member repository 에 save라는 메서드가 어떤 동작을 할 지 구현해줘야 합니다.

    @Test
    @DisplayName("join 테스트 성공")
    void join() {
        // given
        Member test = new Member("test");

        when(memberRepository.save(any())).thenAnswer(invocation -> {
            Member m = invocation.getArgument(0);
            return m;
        });

        when(memberRepository.findById(test.getId())).thenReturn(Optional.of(test)); // <- 이 부분 추가

        // when
        String joinTestId = memberService.join(test);

        // then
        Optional<Member> testMember = memberRepository.findById(joinTestId);
        assertTrue(testMember.isPresent());
        assertEquals(test.getId(), testMember.get().getId());
    }

 

또한 TestContext Framework와도 함께 사용할 수 있습니다.

아래와 같이 @SpringBootTest를 쓰고 빈 컨테이너에 있는 memberService를 @Autwired으로 가져 온 후 테스트 하는 코드 입니다.이 경우에도 Mock으로 구현된 MemberRepository에도 코드를 작성해줘야합니다. 

@SpringBootTest
class MemberServiceTest {

    @MockitoBean
    private MemberRepository memberRepository;

    @Autowired
    private MemberService memberService;

    @Test
    void join() {
        // given
        Member test = new Member("test");

        when(memberRepository.save(any())).thenAnswer(invocation -> {
            Member m = invocation.getArgument(0);
            return m;
        });

        when(memberRepository.findById(test.getId())).thenReturn(Optional.of(test)); // <- 이 부분 추가

        // when
        String joinTestId = memberService.join(test);

        // then
        Optional<Member> testMember = memberRepository.findById(joinTestId);
        assertTrue(testMember.isPresent());
        assertEquals(test.getId(), testMember.get().getId());
    }

}

 

MemberRepository도 @Autowired 하는게 낫지 않나? 생각이 들 수 있습니다.

둘의 차이는 어떤 방식으로 테스트 할 것인가에 따라 다릅니다.

@MockitoBean : 테스트 대상 이외의 의존 객체는 가짜로 대체하고 싶은 경우

@Autowired : 실제 스프링 빈을 주입받아 통합 테스트를 하고 싶은 경우

 

아래는 Mockito 프레임워크를 사용하지 않고 스프링 빈을 주입 받아 통합 테스트 한 경우 입니다.

@SpringBootTest
class MemberServiceTest {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private MemberService memberService;

    @Test
    void join() {
        // given
        Member test = new Member("test");

        // when
        String joinTestId = memberService.join(test);

        // then
        Optional<Member> testMember = memberRepository.findById(joinTestId);
        assertEquals(test.getId(),testMember.get().getId());
    }

}

따라서 테스트 시 원하는 방식에 따라 Mockito 프레임워크를 사용 여부를 결정하면 됩니다.

- 테스트 지원 모듈 

spring boot 기반 애플리케이션의 테스트를 쉽게 작성 할 수 있도록 지원하는 테스트 모듈들이 있습니다.

[ spring boot test ]

 spring boot 테스트를 간소하가 위한 spring의 전문화된 모듈입니다. 테스트 효율성을 높이기 위한 여러 어노테이션과 기능을 제공합니다. 스프링부트 테스트 모듈은 다음과 같은 특징이 있습니다.

 

- 자동 구성 : 존재하는 의존성을 기반으로 테스트 환경을 자동으로 구성합니다.

- WebMvcTest : 스프링 MVC 컨트롤러 테스트에 집중하며, 애플리케이션 컨텍스트의 관련 부부만 로드합니다.

- DataJpaTest: 내장 데이터베이스와 함께 JPA 관련 테스트를 자동으로 구성합니다.

- MockMvc : 스프링 MVC 애플리케이션 테스트를 위한 강력한 모의 환경을 제공합니다.

 

[ Rest Assured ]

 Rest Assured는 HTTP 요청/응답을 간단하게 테스트하는 자바 라이브리러입니다.

Rest Assured를 사용하여 간단하게 테스트 하는 데 유용하지만, 복잡한 구조나 세밀한 비즈니스 로직 검증에는 다소 제약이 있습니다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MemberIntegrationTest {

    @LocalServerPort
    int port;

    @BeforeEach
    void setUp(){
        RestAssured.port = port;
    }

    @Test
    void 회원가입() {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("name", "test");

        RestAssured.given().contentType(ContentType.JSON)
                .body(requestBody)
                .when().post("/members")
                .then()
                .statusCode(201)
                .body("id", notNullValue()); // 검증하는 부분이 빈약
    }
}

 

- 느낀 점

test 방법은 미루다 미루다 이제서야 제대로 각 잡고 공부 했습니다. 확실히 개념들을 잘 정리를 하고 이해하는 것이 중요한 것 같습니다. 특히 저는 이번에 공부하면서 스스로 '왜?' 라는 질문을 하고 답을 찾아갔습니다. 

'왜 Test를 해야 하는 걸까?'  '왜 테스트 유형이 나누는 걸까?' 

그냥 외우는 것보다 질문을 하고 답을 찾아가는 것이 시간이 더 많이 들었지만, 몰랐던 개념들을 나만의 논리로 이해할 수 있게 되었던 시간이였습니다. 꾸준히 공부하고 그냥 그렇게 되는거지가 아니라 왜? 라는 질문을 하며 공부하는 자세가 중요한 것 같다는 생각이 들었습니다.

 

- 예제로 쓰인 전체 코드 

[ 개발 환경 ]

spring boot : 3.5.3

java : 17 

dependency : spring web , rest-assured 5.4.0

database : 인메모리 방식으로 구현

 

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

public class IdGenerator {
    public static String generateId(){
        // 현재 시간 형식: 20250708-103455123 (년월일-시분초밀리초)
        String timePart = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmssSSS"));

        // 랜덤 UUID 일부 사용
        String uuidPart = UUID.randomUUID().toString().substring(0, 6); // 짧게 잘라서 사용

        // 특수문자 포함 조합
        return timePart + "#" + uuidPart;
    }
}
public class Member {
    private String id;
    private String name;

    // 생성자, getter
    public Member(String name) {
        this.id = IdGenerator.generateId();
        this.name = name;
    }

    public String getId() { return id; }

    public String getName() { return name; }
}
public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(String id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
    void clear();
}
@Repository
public class MemoryMemberRepository implements MemberRepository{
    private final Map<String, Member> store = new HashMap<>();

    @Override
    public Member save(Member member) {
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(String id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(m -> m.getName().equals(name))
                .findFirst();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    @Override
    public void clear() {
        store.clear();
    }
}
@Service
public class MemberService {
    private final MemberRepository memberRepository;

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

    public String join(Member member) {
        validateDuplicateMember(member);
        return memberRepository.save(member).getId();
    }

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

    public Optional<Member> findOne(String id) {
        return memberRepository.findById(id);
    }

    public List<Member> findAll() {
        return memberRepository.findAll();
    }
}
@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

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

    @PostMapping
    public ResponseEntity<?> join(@RequestBody Member member) {
        String id = memberService.join(member);
        return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("id", id));
    }

    @GetMapping("/{id}")
    public ResponseEntity<Member> get(@PathVariable String id) {
        return memberService.findOne(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}