๐ก ๋ณธ ๊ฒ์๊ธ์ ๊น์ํ๋์ ์ธํ๋ฐ(Inflearn) ๊ฐ์ ์คํ๋ง ์ ๋ฌธ - ์ฝ๋๋ก ๋ฐฐ์ฐ๋ ์คํ๋ง ๋ถํธ, ์น MVC, DB ์ ๊ทผ ๊ธฐ์ ์ ๋ํ ์๊ฐ ๊ธฐ๋ก์ฉ์ผ๋ก ์์ฑ๋์์ต๋๋ค.
1. ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ ์ ๋ฆฌ
โ ์์ง ๋ฐ์ดํฐ ์ ์ฅ์๊ฐ ์ ์ ๋์ง ์์๋ค๋ ์ ์ ํ์ MemberRepository๋ฅผ ์ธํฐํ์ด์ค(interface)๋ก ๋ง๋ค ๊ฒ์ด๋ค. ํฅํ์ JDBC, MyBatis, JPA ๋ฑ์ผ๋ก ๋ฐ๊ฟ ๋ผ์ธ ์ ์๋๋ก ์ธํฐํ์ด์ค(interface)๋ก ์ค์ ํด๋์.
- ์ปจํธ๋กค๋ฌ(controller): API ์์ฑ ์ ์ฐ์ด๋ ๋ฑ ์น MVC ์ปจํธ๋กค๋ฌ ์ญํ
- ์๋น์ค(service): ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง ๊ตฌํ
- ๋ฆฌํฌ์งํ ๋ฆฌ(repository): ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ์ฌ ๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ DB์ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ
- ๋๋ฉ์ธ(domain): ํ์,์ฃผ๋ฌธ,์ฟ ํฐ ๋ฑ๊ณผ ๊ฐ์ด ์ฃผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ ๋ฐ ๊ด๋ฆฌ๋๋ ๋น์ฆ๋์ค ๋๋ฉ์ธ ๊ฐ์ฒด
2. ํ์ ๋๋ฉ์ธ๊ณผ ๋ฆฌํฌ์งํ ๋ฆฌ ๋ง๋ค๊ธฐ
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
...
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
...
}
- ํ์ ๋ฆฌํฌ์งํ ๋ฆฌ ๋ฉ๋ชจ๋ฆฌ ๊ตฌํ์ฒด์ด๋ค. ์ค๋ฌด์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๊ณ ๋ คํด HashMap ๋์ ConcurrentHashmap์, Long ๋์ AtomaticLong ์ฌ์ฉํจ์ ์ ๋ ํ์. ์ฌ๊ธฐ์๋ ๊ฐ๋จํ ์์ ๋ฅผ ๋ง๋ค๊ธฐ ์ํจ์ด๋ฏ๋ก HashMap, Long์ด ์ฐ์ธ๋ค.
- Optional<T>๋ NPE(NullPointerException)๋ฅผ ๋ฐฉ์งํ ์ ์๋๋ก ๋์์ฃผ๋ ๋ํผ ํด๋์ค(wrapper class)์ด๋ค.
- Optional<T> ํด๋์ค์ ์ ์๋ private final T value; ํ๋๋ ๊ฒฐ๊ณผ๊ฐ null์ด ์๋ ๊ฒฝ์ฐ์ ํํ์ฌ ๊ทธ ๊ฐ(value)์, null์ผ ๊ฒฝ์ฐ ์กด์ฌํ๋ ๊ฐ์ด ์์์ ๋ํ๋ธ๋ค(If non-null, the value; if null, indicates no value is present).
- ๊ฒฐ๊ณผ๊ฐ์ผ๋ก null์ด ๋ฐํ๋ ๊ฐ๋ฅ์ฑ์ด ์์ ๋๋ Optional.ofNullable()๋ก ๊ฐ์ธ์ ์ฒ๋ฆฌํ ์ ์๋ค.
3. ํ์ ๋ฆฌํฌ์งํ ๋ฆฌ ํ ์คํธ ์ผ์ด์ค ์์ฑ
3-1. ํ ์คํธ ํด๋์ค ์์ฑ
- ํ
์คํธ ํด๋์ค๋ src/test/java ํ์ ํด๋์ ์์ฑํ๋ฉฐ, ํ
์คํธ ํด๋์ค์ ์ด๋ฆ ๋์ 'Test'๋ฅผ ๋ถ์ธ๋ค.
- โ ํจํค์ง์ ํด๋์ค๋ฅผ ์ฐจ๋ก๋ก ์ง์ ์์ฑํ๊ฑฐ๋, โก์ธํ ๋ฆฌ์ ์ด(IntelliJ) ํ๊ฒฝ์ด๋ผ๋ฉด ํ ์คํธ ๋์์ด ๋๋ ํด๋์ค์์ ๋จ์ถํค(ctrl + shift + t) ๋๋ฌ Create New Test ์ฒ๋ฆฌํ ์ ์๋ค.
- ํ ์คํธ ํด๋์ค๋ ๊ตณ์ด public์ผ๋ก ์ ์ธํ์ง ์์๋ ๋๋ค.
- ํ ์คํธ ์ผ์ด์ค ์ด๋ฆ์ ํ๊ธ๋ก ์์ฑํด ๋ณด๋ค ์ง๊ด์ ์ผ๋ก ์ธ ์๊ฐ ์๋ค.
- ํ ์คํธ ์ผ์ด์ค๋ง๋ค given/when/then์ผ๋ก ๊ตฌ๋ถ์ง์ด ์ฃผ์์ ๋ฌ์๋๋ฉด ์ข๋ค.
- ํ ์คํธ๋ ์ ์ ํ๋ก์ฐ๊ฐ ์ค์ํ ๋งํผ ์์ธ ํ๋ก์ฐ๋ ์ค์ํ๋ค. ์ฆ ๋ง๋ ๋ก์ง๋ง ๊ฒ์ฌํ๋ ๋ฐ ๊ทธ์น์ง ์๊ณ ์ผ๋ถ๋ฌ ์์ธ๋ฅผ ๋ฐ์์ํค๊ณ , ์์ธ๊ฐ ๋น์ ๊ฒฐ๊ณผ ์ญ์ ํ์ธํด์ผ ํ๋ ๊ฒ์ด๋ค.
3-2. Assertions(JUnit/AssertJ): assertThat
- Assertions์ ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก์ JUnit๊ณผ AssertJ๊ฐ ์๋ ์ํฌํธ(import) ๋ชฉ๋ก์ ์๋ฆฌํ๋ค.
Assertions.assertEquals(result, member);
- JUnit ๊ธฐ๋ฐ์ผ๋ก๋ assertEquals(a, b)๋ฅผ ์จ์ a๊ฐ b ๋ง๋์ง๋ฅผ ํ์ธํ ์ ์๋ค.
assertThat(result).isEqualTo(member);
assertThat(result.size()).isEqualTo(2);
- AssertJ์์๋ assertThat(a).isEqualTo(b)๋ก ์์ฑํ๋ค. ์ธ์ ํ์ ์ ๋ง๋ Assert ํด๋์ค๋ฅผ ๋ฐํํด ํ์ํ ๋ฉ์๋๋ง ๋ถ๋ฅ๋ผ ์๊ณ , ๋์๊ฐ ๋ฉ์๋ ์๋์์ฑ์ด ์ง์๋ผ ํธ๋ฆฌํ๋ค๋ ์ฅ์ ์ด ์๋ค. ์ด๋ฒ ๊ฐ์์์๋ AssertJ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ํฌํธ(import)ํด ์ฐ๋๋ก ํ๋ค.
- Assertions์ ๋จ์ถํค(alt + enter) ์ณ์ static์ผ๋ก ์ ์ธํด๋๋ฉด Assertions.assertThat() → assertThat์ผ๋ก ๊ฐํธํ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
3-3. @AfterEach
class MemoryMemberRepositoryTest {
MemoryMemberRepository respository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
respository.clearStore();
}
}
- @AfterEach ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ฉด ๊ฐ ํ ์คํธ๊ฐ ์ข ๋ฃ๋ ๋๋ง๋ค ๋ช ์๋ ๊ธฐ๋ฅ์ด ์คํ๋๋ค.
- ํ ์คํธ ๋ฉ์๋์ ์์๋ ๋ณด์ฅ๋์ง ๋ชปํ๋ฉฐ, ๊ฐ๊ฐ์ ํ ์คํธ ๋ฉ์๋๋ ์์์ ๊ด๊ณ ์์ด, ์๋ก๊ฐ ์์กด ๊ด๊ณ ์์ด ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋์ด์ผ ๋ง๋ ํ๋ค. ๋ฐ๋ผ์ ํ ์คํธ๊ฐ ์ถฉ๋์ด ์๋๋ก ํ ์ฐจ๋ก ์คํ๋ ๋ค ๊ณต์ฉ ๋ฐ์ดํฐ๋ฅผ ๊น๋ํ๊ฒ ์ง์์ฃผ๋ ์์ ์ด ์๊ตฌ๋๋ค.
3-4. TDD
โ ๊ฐ์ ๋ด์ฉ๊ณผ๋ ๋ค๋ฅด์ง๋ง, ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ์ ๋ํ ์ค๋ช ์ ๋ง๋ถ์ธ๋ค. ํ ์คํธ ๊ด๋ จํด์๋ ๊ผญ ๊น์ด ์๊ฒ ๊ณต๋ถํด๋ณด๊ธฐ๋ฅผ ๊ถ์ฅํ๋ ๋ฐ์ด๋ค.
- ํ ์คํธ ํด๋์ค๋ฅผ ์์ฑํ ๋ค์ → ๋ฆฌํฌ์งํ ๋ฆฌ(repository)๋ฅผ ์์ฑํ๋ ์์๋ก ๋ค์ง์ ์๋ ์๋ค. ์๋ฅผ ๋ค์ด ๋ณ๋ชจ์ ์ํ์ ๋ง๋ค๊ณ ์ ํ ๋ ๊ทธ๊ฒ์ ๊ฒ์ฆํ ์ ์๋ ํ ๋จผ์ ๋ง๋ค์ด๋๊ณ → ์ํ์ ๋น์ด์ ๊ทธ ํ์ ๋ง๋์ง ์ ๋ง๋์ง ํ์ธํ ์๋ ์๋ค๋ ๋ป์ด๋ค.
- ํ ์คํธ๋ฅผ ์์ฑํ ๋ค ๊ตฌํ ํด๋์ค๋ฅผ ๋ง๋๋ ์์ ๊ณผ์ ์ ์ผ์ปฌ์ด ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ(TDD, Test Driven Development)๋ผ๊ณ ๋ถ๋ฅธ๋ค.
4. ํ์ ์๋น์ค ๊ฐ๋ฐ
4-1. DI ์ ์ฉ
โ ์์กด์ฑ ์ฃผ์ (DI, Dependency Injection) ํตํด ์ง์ ์ ์ธ ๊ฒฐํฉ ๊ด๊ณ๋ฅผ ํผํ๊ณ ์ ์ง๋ณด์์ฑ์ ๋์ผ ์ ์๋ค.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
- MemberService๊ฐ MemoryMemberRepository ์ธ์คํด์ค๋ฅผ ์ง์ ์์ฑํ ์์์ด๋ค. ์๋์ ๊ฐ์ด ๊ฐ์ ์ด ํ์ํ๋ค.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
- MemberRepository์ ๋ํ ์์กด์ฑ์ ์ฃผ์ ๋ฐ๋๋ก ์์ฑํ ์์์ด๋ค.
- ์๋น์ค ํ ์คํธ์ ์๋น์ค ํด๋์ค๊ฐ์๋ ๊ฐ์ ์ธ์คํด์ค๋ฅผ ์ฐธ์กฐํ ์ ์๋๋ก ํด์ผ ์๋ง์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์๋ค. ๊ฐ๊ฐ ๋ค๋ฅธ ์ธ์คํด์ค๋ฅผ ๋ํ๊ณ ์๋ค๋ฉด ๋ฐ์ดํฐ๋ง์ ์์ถฉํ๋ ๊ฒฐ๊ณผ๋ก ์ด์ด์ง ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
4-2. ๋ฉ์๋ ์ถ์ถ
public Long join(Member member) {
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("์ด๋ฏธ ์กด์ฌํ๋ ํ์์
๋๋ค.");
});
memberRepository.save(member);
return member.getId();
}
- Optional<T> ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ฌ์ค ๊ฐ๋ ์ฑ ๋ฌธ์ ๋ก ๊ถ์ฅ๋์ง ์๋ ๋ฐ์ด๋ค. ๋ฐ๋ผ์ ์ฐธ์กฐ ๋ณ์๋ฅผ ๋ง๋ค์ด ํ ๋นํ๊ธฐ ๋ณด๋ค๋ ์๋์ ๊ฐ์ด ์ง์ ์์ฑํ๋ ๊ฒ์ด ์ข๋ค.
- ๋, Optional<T> ์ธ์คํด์ค์์ ๊ฐ์ ํธ์ถํ ๋ ์ญ์ get()๋ณด๋ค๋ orElseGet()์ ์ฌ์ฉํด ๊ฐ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ์๋ง ๊บผ๋ด์ค๋๋ก ์ฐ๋ ๊ฒ์ด ๋ฐ๋์งํ๋ค.
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("์ด๋ฏธ ์กด์ฌํ๋ ํ์์
๋๋ค.");
});
}
- ์ธํ ๋ฆฌ์ ์ด(IntelliJ)์์ ๋ฉ์๋ ๋ก์ง ์ถ์ถํ ๋๋ ํด๋น๋๋ ๋ฒ์๋งํผ ๋๋๊ทธํ ํ → ๋จ์ถํค(alt + enter)๋ฅผ ๋๋ฌ ์ ํ์ง๋ก ์๊ธด ๋ฉ์๋ ์ถ์ถ(Extract Method)์ ํ์ฉํ๋ฉด ๋๋ค.
5. ํ์ ์๋น์ค ํ ์คํธ
5-1. @BeforeEach
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
}
- @BeforeEach ์ด๋ ธํ ์ด์ ์ ๋ช ์ํ ๋ฉ์๋๋ ๊ฐ ํ ์คํธ ์คํ ์ ์ ํธ์ถ๋๋ค.
- ์ด๋ฅผ ํตํด ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , ์์กด ๊ด๊ณ ์ญ์ ํญ์ ์๋กญ๊ฒ ๋ถ์ฌ๋ผ ํ ์คํธ ์ผ์ด์ค๊ฐ ๋ ๋ฆฝ์ ์คํ์ ๊ธฐ๋ฐ์ ๋ฆ๋๋ค.
5-2. Assertions(AssertJ): assertThrows
try {
memberService.join(member2);
fail();
} catch(IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("์ด๋ฏธ ์กด์ฌํ๋ ํ์์
๋๋ค.");
}
- ๊ฐ์ ์ด๋ฆ์ ๊ฐ์ง member1, member2๊ฐ ์๋ค. ์ค๋ณต ํ์ ๊ฒ์ฆ ์ค ์์ธ๊ฐ ์ ๋๋ก ๋ฐ์ํ๋์ง ํ ์คํธ๋ฅผ ์งํํด ๋ณด์.
- ์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ try-catch๋ฌธ์ผ๋ก ๊ฐ์ผ ๋ค assertThat(a).isEqualTo(b)); ํตํด ๋ฐ์ํ ์์ธ ๋ฉ์์ง๊ฐ ์์๊ณผ ๊ฐ์์ง ๊ฒ์ฆํ๋ ๊ฒ์ด๋ค.
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("์ด๋ฏธ ์กด์ฌํ๋ ํ์์
๋๋ค.");
- ๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ assertThrows(์์ธ, () -> ๋ก์ง)๋ฅผ ํ์ฉํ๋ ๊ฒ์ด๋ค. ํ์ฌ ์ฃผ์ด์ง ์กฐ๊ฑด์์ ๋ก์ง์ ์คํํ๊ฑฐ๋ ์ด๋ฌํ ์์ธ๊ฐ ๋ฐ์ํ๋ค๋ ์๋ฏธ๋ก ๊ธฐ์ ํ๋ค.
- ๋ฐ๋ผ์ ๋ช ์๋ ์์ธ์ ์ผ์นํ ๊ฒฝ์ฐ ์ฑ๊ณต, ๋ถ์ผ์นํ๋ฉด ์คํจ๋ก ์ด์ด์ง๋ค.
- ๋ฉ์์ง์ ๋ํ ๊ฒ์ฆ ์ญ์ ๊ฐ๋ฅํ๋ค. assertThrows ๊ฒฐ๊ณผ๋ฅผ ์ธ์คํด์ค๋ก ๋ง๋ค์ด์ค ๋ค → assertThat์ผ๋ก ๊ธฐ๋ํ๋ ๋ฉ์์ง๊ฐ ๋ง๋์ง ํ ๋ฒ ๋ ํ์ธํ๋ ๋ฐฉ๋ฒ์ ํตํ๋ค.
- ์ธํ ๋ฆฌ์ ์ด(IntelliJ) ํ๊ฒฝ์์ ์ธ์คํด์ค๋ฅผ ์์ฑํ ๋๋ ๋จ์ถํค(ctrl + alt + v)๋ฅผ ์ธ ์ ์๋ค.