| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 프론트엔드
- HeidiSQL
- 코딩테스트
- 주석 단축키
- route53
- 데이터베이스
- #AwsCloudClubs
- 코드트리
- react
- 학습회고
- 오블완
- javascript
- 티스토리챌린지
- 기술심화
- 유니티
- SQL
- 박스 모델
- 깃허브뱃지
- CSS
- GitHub
- html
- #UMC #UMC10기 #UMC 블로그챌린지 #IT동아리
- 깃허브
- 유니티 모듈
- 코드트리조별과제
- 유니티 에디터
- 유니티 안드로이드 빌드
- 웹 개발
- 파이썬
- vscode
- Today
- Total
Hello It's good to be back ^_^
[ECC-백엔드 5팀] 8주차 스터디 - 도메인, 레포지토리, 컨트롤러, 서비스, 테스트, 의존성 주입, 스프링 빈, 컴포넌트 스캔 본문
[ECC-백엔드 5팀] 8주차 스터디 - 도메인, 레포지토리, 컨트롤러, 서비스, 테스트, 의존성 주입, 스프링 빈, 컴포넌트 스캔
HongyeonLee 2026. 5. 7. 17:52범위: 섹션 4-5
실습: https://github.com/HongYeonLee/ECC-Backend-Study
GitHub - HongYeonLee/ECC-Backend-Study: ECC 51기 SS 백엔드 5팀 스터디 내용을 기록하는 레포지토리입니다
ECC 51기 SS 백엔드 5팀 스터디 내용을 기록하는 레포지토리입니다. Contribute to HongYeonLee/ECC-Backend-Study development by creating an account on GitHub.
github.com
섹션 4. 회원 관리 예제 - 백엔드 개발
1. 회원 관리 예제 - 백엔드 개발
1.1 비즈니스 요구사항 정리
데이터: 회원ID, 이름
기능: 회원 등록, 조회
DB: 아직 선정되지 않음
일반적인 웹 애플리케이션 계층 구조

- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 핵짐 비즈니스 로직 구현 ex. 중복 회원 검사
- 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리함
- 도메인: 비즈니스 도메인 객체 ex. 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계

- 아직 DB가 선정되지 않아서 우선 인터페이스로 구현하고 나중에 선정되면 구현 클래스를 변경할 수 있게(바꿔끼움) 설계
- DB는 RDB, NoSQL, JDBC 등등 다양한 저장소를 고민중인 상황으로 가정
- 개발을 진행하기 위해 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 DB를 사용하고 나중에 DB가 선정되면 인터페이스를 바꿈
1.2 회원 도메인과 리포지토리 만들기
src/main/java/hongyeon_spring/domain 패키지 생성
Member 클래스 (회원을 구현하기 위한 클래스)
package hongyeon_spring.domain;
public class Member {
private Long id; //고객이 정하는게 아니라 시스템이 정하는 고객아이디
private String name; //고객이 회원가입할 때 적는 내용
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;
}
}
- 고객의 필드로는 id(시스템이 정함)와 name(고객이 정함)이 있음
src/main/java/hongyeon_spring/repository 패키지 생성
MemberRepository 인터페이스
package hongyeon_spring.repository;
import hongyeon_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member); //회원을 저장하면 저장된 회원이 리턴
Optional<Member> findById(Long id); //리턴값이 null인 경우(회원이 null) optional로 감싸서 리턴
Optional<Member> findByName(String name);
List<Member> findAll();
}
- 저장소의 인터페이스를 정함
- 고객 객체 저장, 고객을 id/name으로 찾음, 모든 저장된 고객을 찾는 메소드가 있음
- Optional 클래스는 리턴 값이 null인 경우 Optional로 감싸서 리턴할 수 있게 해주는 것임
MemoryMemberRepository 클래스 (고객을 저장하는 저장소를 구현하는 클래스)
package hongyeon_spring.repository;
import hongyeon_spring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); //멤버 아이디와 멤버 객체 저장, 실무에서는 공유 변수일 때는 동시성 문제로 Concurrent HashMap을 씀
private static long sequence = 0L; //012..키 값을 생성해줌, 실무에서는 동시성 문제로 atomic long 사용
@Override
public Member save(Member member) {
member.setId(++sequence); //저장소에 넣기 전에 시스템이 고객아이디 결정, name이 고객이 이미 입력해서 저장함
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //null 리턴될 경우를 위해 optional로 감쌈
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name)) //고객의 이름과 매개변수로 받은 이름이 같은지 확인, 같은 경우만 필터링됨
.findAny(); //돌면서 하나라도 찾으면 바로 리턴, 끝까지 돌렸는데 없으면 optional에 null이 포함되서 리턴
}
@Override
public List<Member> findAll() {
//맵을 리스트로 변환해서 리턴
return new ArrayList<>(store.values());
}
}
저장소 변수 store
- 고객 객체는 HashMap으로 저장, 키로는 id, 값으로는 고객 객체를 저장함
- 실무에서는 고객과 같은 공유 변수를 이용할 때는 동시성 문제 때문에 Concurrent HashMap을 사용함
고객의 id 변수 sequence
- id는 시스템이 정하는 변수값으로 long 타입의 sequence라는 변수를 사용함
- 마찬가지로 동시성 문제로 실무에서는 AtomicLong 사용
고객 객체를 저장하는 메소드 save
- 고객 객체를 저장하는 save 메소드는 매개변수로 고객 객체를 받고, 고객의 id(sequence)를 정함
- 해시맵 store에 키로 id, 값으로 고객 변수를 저장함
- 저장한 고객 객체를 리턴함
고객을 id로 찾는 메소드 findById
- 매개변수로 고객의 아이디를 받음
- store에서 키 값에 전달받은 id를 넣어서 찾음
- 만약 null값이 나온다면 optional로 감싸서 리턴
고객을 name으로 찾는 메소드 findByIName
- 매개변수로 고객의 이름을 받음
- store.values().stream().filter(member -> member.getName().equals(name)) - store에서 값을 찾는데, 매개변수로 받은 name과 같은 값을 가진 데이터만 필터링함
- .findAny() - 필터링된 데이터 중 하나라도 찾으면 바로 리턴, 전부 다 돌았는데도 없으면 optional에 null을 넣어서 리턴
모든 고객을 찾는 메소드 findAll
- 저장소 변수인 store는 HashMap 타입임. 그러나 실무에서는 편리함 때문에 리스트를 많이쓰기에 해시맵을 리스트로 변환해줘야함
- new ArrayList<>(store.values()) - store에 저장된 값(데이터)들을 새 리스트에 만들어서 넣어주고 리턴
1.3 회원 리포지토리 테스트 케이스 작성
src/test/java/hongyeon_spring/repository에 MemoryMemberRepositoryTest 클래스 생성
package hongyeon_spring.repository;
import hongyeon_spring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach //각 테스트 메소드들이 끝난 뒤에 호출되는 콜백 메소드, 테스트 데이터 지우기 위함
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get(); //optional에서 값을 꺼내야 하므로 get() 이용
// System.out.println("result = " + (result == member));
// Assertions.assertEquals(member, result); //매개변수로 기대값, 실제값 전달. 둘이 동일한지 검사해줌
// Assertions.assertEquals(member, null);
// Assertions.assertThat(member).isEqualTo(result);
assertThat(member).isEqualTo(result); //static으로 클래스 넣어서 더 편하게 작성
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
- @AfterEach - 각 테스트 메소드들이 끝난 후에 실행되는 콜백 함수로, 테스트 메소드간의 의존성이 생기는 걸 방지하기 위해 테스트 데이터를 지우는 역할의 메소드를 작성한다.
- @Test - 메소드앞에 붙이면 바로 해당 메소드를 실행할 수 있게 해줌
- get() - Optional로 감싸진 데이터를 꺼낼 때 사용하는 메소드
- Assertions.assertEquals(기대값, 실제값) - 테스트를 위해 실제로 데이터 값들이 같은지 확인할 때 사용
- Assertions.assertThat(기대값).isEqualtTo(실제값)
import static org.assertj.core.api.Assertions.*;
- static으로 선언하면 앞에 Assertions 안붙이고 바로 assertThat 메소드 사용 가능
테스트 케이스
- 클래스 단위로 실행이 가능해서 여러 테스트를 동시에 할 수 있음
- TDD(Test Driven Development)- 테스트 주도 개발로, 테스트를 먼저 만들고 그 다음에 구현 클래스를 만들어서 테스트를 돌리는 것
- 테스트가 수십, 수백개로 엄청 많아질 경우, 테스트 패키지 자체를 빌드해서 돌리면 편하다
1.4 회원 서비스 개발
src/main/java/service에 MemberService 클래스 작성
package hongyeon_spring.service;
import hongyeon_spring.domain.Member;
import hongyeon_spring.repository.MemberRepository;
import hongyeon_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 회원가입
public Long join(Member member) {
//같은 이름의 중복 회원x
// Optional<Member> result = memberRepository.findByName(member.getName());
// result.ifPresent(m -> { //null이 아니라 값이 있으면 동작(optional 때문에 사용), 들어온 값(멤버) m, 과거엔 result == null 이런식으로 썼는데 optional 덕분에 ifPresent로 작성
// throw new IllegalStateException("이미 존재하는 회원입니다");
// });
//만약 optional로 감싸진 멤버 객체를 바로 꺼내고 싶으면 Member member = result.get()해서 꺼내면 됨, 권장x
// result.orElseGet(), 값이 있으면 optional에서 꺼내고 없으면 특정 메소드를 실행하거나 디폴트 값을 넣어서 꺼내거나
//바로 ifPresent를 사용할 수도 있음 어차피 optional로 리턴하니까
//이렇게 멤버를 가져오는 덩어리를 메소드화 시키는 것이 좋음
// memberRepository.findByName(member.getName())
// .ifPresent(m -> {
// throw new IllegalStateException("이미 존재하는 회원입니다.");
// });
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
회원가입 기능 join 메소드
- 리포지토리에 고객을 저장하면 됨
- 그 전에 중복된 이름을 가진 고객은 없다고 했을 때, 중복 고객을 검사하는 로직이 필요
- 이름으로 고객을 가져올 경우 optional에 감싸져서 들어오므로 optional에서 한번 꺼내는 과정이 필요함
- .ifPresent(m -> {}) - 꺼내온 값이 null이 아니라 값이 있을 경우(동일한 이름을 가진 고객이 이미 있음) 실행시킬 함수를 작성
- 중복 고객 검사 로직은 따로 메소드로 빼는 것이 좋음

* 리팩토링 하고 싶은 코드 부분을 선택하고 ctrl + alt + shift + t
서비스 기능은 메소드 이름을 비즈니스 의존적으로 작성해야 한다
리포지토리는 메소드 이름을 이름을 단순하고 기계적이게 작성한다
1.5 회원 서비스 테스트
ctrl + shift + t

- 해당 클래스에 대한 테스트 클래스를 만들어줌

- 라이브러리로 JUnit5 선택
- 테스트 할 메소드 선택
src/test/java/hongyeon_spring/service에 MemberServiceTest 클래스 구현
테스트 코드는 실제로 빌드에 포함되는 코드가 아니기에 직관적으로 한글로 작성해도 괜찮다
package hongyeon_spring.service;
import hongyeon_spring.domain.Member;
import hongyeon_spring.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 java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.fail;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
//각 테스트 실행 전 마다
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); //DI - Dependency Injection
}
@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 - 각 테스트 실행 전마다 실행시키는 메소드 어노테이션
- given, when, then 문법 - 테스트를 할 때 주어진 값을 언제 어떻게 하면 어떤 결과가 나와야하는지 한 눈에 파악할 수 있도록 함
- try-catch는 예외가 발생하면 이를 복구하거나 우회하기 위해 설계된 문법으로써, 이 상황에서 예외가 발생하는지 검증하는 테스트 코드의 의도와는 적합하지 않음. 또 fail()을 빼먹을 수도 있음
- JUnit5의 철학은 테스트 코드를 테스트답게 작성하는 것
- 따라서 assertThrows를 이용해서 이 로직을 실행하면 반드시 이 예외가 터져야한다는 의도를 명확히 보여주는 것이 좋음
assertThrows(IllegalStateException.class, () -> memberService.join(member2))
- memberService.join(meber2)를 실행했을 때 IllegalStateException 예외가 발생한다는 의미
- assertThrow는 발생한 예외 객체를 리턴함
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
- 발생한 에외 객체를 변수 e에 담고 메세지를 가져와 검증
MemberService 클래스에 의존성 주입
private final MemberRepository memberRepository;
//멤버 서비스에서 사용하는 리포지토리와 테스트에서 사용하는 리포지토리가 다른 객체이므로 발생하는 문제를 방지하기 위해 생성자를 이용
// DI - Dependency Injection로 외부에서 넣어주는 거, 멤버 서비스 입장에선 외부에서 리포지토리를 넣어줌
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
- 서비스 클래스에서 리포지토리를 new해서 생성하고, 테스트 클래스에서 리포지토리를 new해서 사용하면 서로 다른 객체를 사용하기 때문에 테스트가 어긋날 수가 있다
- 따라서 서비스 클래스에서 사용하는 리포지토리는 외부에서 생성자를 통해 넣어질 수 있도록 구현하는게 좋다.
- 이를 의존성 주입이라고 한다.
의존성 주입 (Dependency Injection, DI)
: 서비스 객체가 자신이 의존하는 리포지토리 객체의 구현체를 직접 생성하지 않고 외부(설정 등)에서 전달받아(생성자의 매개변수로 줌) 사용하는 설계 방식
섹션5. 스프링 빈과 의존관계
2.1 컴포넌트 스캔과 자동 의존관계 설정
스프링 빈
: 스프링이 열릴 때 스프링 컨테이너라는 것이 생긴다. @Component라는 어노테이션이 있으면 그 어노테이션이 달린 클래스의 타입으로 객체를 만들어 컨테이너에 넣고 관리한다. 이렇게 생성된 객체를 스프링 빈이라고 한다.
@Component
: 해당 어노테이션이 있으면 컴포턴트 스캔을 통해 자동으로 스프링 빈으로 등록된다. @Service, @Repository, @Controller 어노테이션은 @Component 어노테이션을 자동으로 포함하고 있다. 이러한 어노테이션이 없으면 순수 자바 코드로 스프링이 인식하지 못하고 컨테이너에 포함되지 않는다.
컨트롤러 - 외부 요청 받음
서비스 - 비즈니스 로직
리포지토리 - 데이터 저장
package hongyeon_spring.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import hongyeon_spring.service.MemberService;
@Controller
public class MemberController {
//service는 new해서 새로운 객체를 만들지 않고 하나만 생성해놓고 공용으로 쓰면 됨
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
super();
this.memberService = memberService;
}
}
- Service는 new로 여러 객체(인스턴스)를 만들어서 사용하지 않고 하나만 만들어서 공용으로 사용하도록 한다 → 클래스에 생성자로 Service 객체를 받는다
- @Autowired : 생성자에 붙이는 어노테이션으로 스프링 컨테이너에 해당 컨트롤러(서비스)가 생성될 때 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. (스프링 빈에 등록된 매칭되는 서비스(리포지토리)의 객체를 찾아 연결해준다. ) → 의존성 주입

컴포넌트 스캔의 대상
- SpringApplication이 위치한 곳에서 하위 계층만 스캔한다. 단, 같은 패키지내여만 한다
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때 기본으로 싱글턴으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 객체(인스턴스)이다. 웬만해선 싱글턴으로 구현한다
2.2 자바 코드로 직접 스프링 빈 등록하기
@Service, @Repository, @Autowired와 같은 어노태이션 없이 객체를 스프링 빈에 등록하는 법
SpringApplication과 같은 위치에 SpringConfig 클래스를 만든다
package hongyeon_spring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import hongyeon_spring.repository.MemberRepository;
import hongyeon_spring.repository.MemoryMemberRepository;
import hongyeon_spring.service.MemberService;
@Configuration
public class SpringConfig {
//스프링이 스프링 빈에 등록함
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
- config 파일에 스프링 빈으로 등록할 클래스의 객체를 생성하는 메소드를 작성하고 앞에 @Bean 어노테이션을 붙인다.
- MemberService의 객체와 MemberRepository는 스프링 컨테이너에 스프링 빈으로 등록된다
- 특히 Repository는 향후 다른 리포지토리로 변경할 예정이므로 컴포넌트 스캔 방식 대신 자바 코드로 스프링 빈을 설정한다.
- 그렇게 하면 memberRepository 메소드의 return 문의 호출 함수의 이름만 나중에 바꿔주면 되기 때문이다
참고사항
- 예전에는 XML 방식으로 스프링 빈을 직접 등록했으나 지금은 사용x
- 의존성 주입(DI)에는 필드 주입, setter 주입, 생성자 주입 3가지가 있다. 그러나 의존 관계가 실행 중에 동적으로 변하는 경우는 아예 없으므로 생성자 주입을 권장
- 필드 주입: @Autowired private MemberService memberserive; // final 사용x
- setter 주입: 이렇게 할 경우 public하게 노출되어서 안 좋음, 중간에 memberService를 바꿀 일이 없는데 굳이 사용x
@Autowired
public void setMemberService(MemberService memberService){
this.memberService = memberService;
}
- 생성자 주입
@Autowired
public MemberController(MemberService memberService) {
super();
this.memberService = memberService;
}
- 실무에서는 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드에는 컴포넌트 스캔을 이용한다
- 정형화 되지 않거나 상황에 따라 구현 클래스를 변경해야 하면(ex.DB 변경) 설정을 통해 스프링 빈으로 등록한다
- 스프링 빈으로 등록하지 않은 객체에 대해서는 @Autowired가 작동하지 않는다
인텔리제이 단축키
* shift + F10 : 이전에 실행했던 걸 그대로 실행함
* ctrl + shift +enter : 바로 다음 줄로 이동
* alt + enter : 코드 추천 사항 보여줌
* shift + F6 : 한번에 변수명 중복되는 것끼리 바꾸기
* ctrl + shift + F10 : 실행
* ctrl + alt + v : 호출하는 함수의 리턴 값에 맞게 자동 작성
'Study > Spring Boot' 카테고리의 다른 글
| [ECC-백엔드 5팀] 10주차 스터디 (0) | 2026.05.23 |
|---|---|
| [ECC-백엔드 5팀] 9주차 스터디 (0) | 2026.05.16 |
| [ECC-백엔드 5팀] 7주차 스터디 - 스프링 입문, 정적 콘텐츠, MVC, API (0) | 2026.05.02 |
| [ECC-백엔드 5팀] 3주차 스터디 (0) | 2026.04.02 |
| [ECC-백엔드 5팀] 2주차 스터디 (0) | 2026.03.25 |
