시험보고 다음날 바로 시험 준비과정에 대한 글을 쓴다. 합격인지 불합격인지도 모르는 상황... 그래도 지금이 가장 준비과정에 대한 생생한 기억이 남아 있으니 준비과정에 대한 회고를 작성해보겠다!
시험: SQL 개발자(SQLD)
회차: 45회
시험 장소: 서울 성동공업고등학교
공부기간: 한달
준비 계기
백엔드 개발자로 진로를 결정하면서 SQL에 대해서 공부할 필요가 있겠다고 생각했다. 그리고 현재 지원한 부트캠프, 싸피가 6월말, 7월에 시작한다는 점을 생각했을 때 5월에 뭔가 생산적인 활동을 하면 좋을 것 같았다. 그래서 5월 한달간 SQLD 자격증을 준비했다.
사실 대학교를 다니면서 데이터베이스 수업을 수강했고 이전 직장에서 기본적인 SQL문을 배웠기 때문에 완전 노베이스 상태는 아니었다. 그래도 기초를 다진다는 생각으로 한달 동안 하루에 한 2~3시간씩 꾸준히 공부한 것 같다. (7일 전사분들도 계신거 같은데... 그래도 제대로 된 SQL 학습이 하고 싶었다) 그 결과 SQL에 대한 기본적인 학습을 할 수 있었고, 어느정도 도움이 된 자격증 공부라고 생각한다.
학습 과정
구글에 SQLD 합격 후기를 검색해서 다른 분들의 발자취를 따라가고자 했다. 몇몇 블로그 글을 읽었고 그 중 잘 정리된 블로그와 카페를 참고했다.
- 참고 자료
해당 블로그를 작성하신 분처럼 2015년에 정리된 자료를 빠르게 읽어 보았다. 조금 예전 자료이긴 하지만 내용은 지금이랑 다르지 않을 것 같다. 처음엔 그냥 이해가 안되는 내용이라도 눈에 익힌다는 느낌으로 빨리 빨리 읽었다.
1개 목차를 읽은 다음에는 노랭이 책으로 해당 목차 문제를 풀었다. 바로 문제를 풀면서 복습하는 과정이 중요하다고 생각했다. 시험을 보고 나서 느끼는 건데 확실히 노랭이 책을 많이 보는게 중요한거 같다. 45회차 시험에서도 완전 동일한 문제가 몇개 나왔다.
흔히 노랭이라고 불리는 책
사실 이렇게 공부를 해도 실행계획이나 인덱스 같은 부분은 이해가 안되는 경우가 있었다. 그럴 때는 유튜브에 국민대학교 김남규 교수님이 강의하시는 SQLD 강의를 참고해서 보았다. 애초에 SQLD 자격증을 위한 강의라서 필요한 부분만 잘풀어서 설명해 주신다. (강의가 길어서 필요한 부분만 골라서 보았다)
노랭이를 풀다보면 이해가 안가는 문제들이 있다. 그때는 구글링과 데이터 전문가 포럼이라는 네이버 카페에 해당 문제 해설을 찾아보았다. 그래도! 이해가 안되면 유투브에 노랭이 문제풀이 영상들을 참고해서 보았다.
이제와서 보니 참 다양한 소스들을 활용해서 공부했다...ㅋㅋ
끗
5월 한달은 싸피 인적성 준비와 SQLD 자격증 준비로 Java나 CS 공부를 많이 하지 못했다 ㅠㅠ 이제 6월부터는 정말 백엔드 개발자가 되기 위한 기본인 Java 공부와 네트워크 공부를 열심히 해볼 생각이다. 어느 교육과정을 가더라도 Java, Spring을 사용할 것이기 때문에 6월 한달은 얘네들을 집중적으로 공부해야겠다.
action="member/new", POST 방식으로 <"name" : 입력받은 값> 형태로 데이터를 넘겨준다.
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
그럼 컨트롤러는 MemberForm 객체를 생성하고 프론트에서 받은 <"name" : 입력받은 값> 을 객체에 저장한다.
이후 컨트롤러에서 또 Member 객체를 만들고 MemberForm의 name값을 받는다.
그리고 이 Member 객체를 memberService.join으로 넘기면 프론트에서 받은 값을 리포지토리에 저장할 수 있게 된다.
마지막으로 "redirect:/"를 통해 다시 홈화면으로 페이지를 변경한다.
이제 입력한 값이 정상적으로 리포지토리에 저장되었는지 확인해보도록 하겠다.
홈화면의 회원 목록 링크를 클릭하면 "/members/memberList" 경로로 이동한다.
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
빈 연결리스트에 노드 추가: 아무것도 존재하지 않으므로 head/tail 모두 추가하려는 노드를 가리키면 된다.
연결리스트의 첫번째에 노드를 추가: 새로운 노드가 head의 주소를 가리킴. head의 주소는 새로운 노드로 변경
연결리스트의 마지막에 노드를 추가: 새로운 노드가 tail의 주소를 가리킴. tail의 주소는 새로운 노드로 변경
연결리스트의 중간에 노드를 삽입: 4개의 링크를 변경하면 된다. 글보단 아래 그림을 보는 것이 이해가 편하다.
-노드 삭제
void remove(Node *p) {
if (head == p && tail == p) { // p is only node of list
head = NULL;
tail = NULL;
}
else if (head == p) { // p is head
p->next->prev = NULL;
head = p->next;
}
else if (tail == p) { // p is tail
p->prev->next = NULL;
tail = p->prev;
}
else { // p is middle node
p->prev->next = p->next;
p->next->prev = p->prev;
}
free(p);
size--;
}
이중 연결리스트의 장점인 삭제 부분이다. 기존 연결리스트는 어떤 노드를 삭제하기 위해서 타겟노드의 이전 노드를 기억하고 있어야 했다. 하지만 노드들 마다 이전 노드의 주소를 알고 있기 때문에 코드에서 이전노드의 주소를 기억할 필요가 없어지게 된다.
노드 삽입/추가와 마찬가지로 4가지 경우로 나누어 코드를 구현하면 된다.
-정렬된 연결리스트에 노드 삽입
add_ordered_list(char *item) { // start searching at tail
Node *p = tail;
while (p != NULL && strcmp(item, p->data) < 0) {
p = p->prev;
}
add_after(p, item);
}
이미 노드 삽입 부분을 구현했다. 따라서 기존 삽입함수를 활용하면 4가지 경우를 고려하지 않고 코드를 구현할 수 있다. 일단 정렬된 연결리스트이기 때문에 삽입하려는 노드의 값보다 작은 값을 찾아야 한다. (여기선 abc 오름차순 정렬) strcmp함수로 삽입하려는 노드보다 작은 값을 찾거나 p 노드가 가리키는 노드가 NULL이면 반복을 종료한다. 이 노드를 add_after 함수에 전달만 하면 끝이다. 4가지 경우는 add_after 함수에서 처리할 것이다.
-결과 확인
int main() {
add_ordered_list("c");
add_ordered_list("a");
add_ordered_list("d");
add_ordered_list("b");
return 0;
}
결과값
제대로 결과값이 나오는 모습을 확인
사실 연결리스트 개념 자체는 이해하기 어렵지 않았다. 다만 c언어를 잘못해서 포인터 변수에 대한 이해가 조금 어려웠던 것 같다.
회원가입 프로세스에서 컨트롤러가 데이터를 입력받고 이를 서비스에 넘겨준다고 하면 이는 컨트롤러와 서비스간에 의존관계가 있다고 말할 수 있다. 스프링에서는 이러한 의존관계를 Annotation을 통해 설정해 줄 수 있다.
-컴포넌트 스캔 방식(@Annotation)과 자동 의존관계 설정
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
@Controller와 같이 Annotation을 달아주면 스프링 어플리케이션은 이 객체를 스프링 컨테이너에 빈으로 등록한다.
(위 코드의 경우 컨트롤러 빈으로 등록)
만약 스프링 컨테이너에 1개의 서비스가 존재하고 여러개의 컨트롤러가 이 서비스를 사용한다고 하면 어떻게 될까?
컨트롤러마다 동일한 서비스 객체를 만들어서 사용해야 할까?
스프링 빈이 있기 때문에 그럴 필요가 없다!!
위의 코드처럼 @Autowired를 통해 @Service로 선언된 아래 코드를 바로 사용할 수 있다!!
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
서비스 또한 마찬가지로 @Repository로 선언된 저장소와 의존관계로 설정해 줄 수 있다.
@Repository
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
}
이게 바로 DI(Dependency Injection)이다. 스프링이 동작하면서 class의 객체를 만들고 자동으로 의존 관계를 주입해준다.
DI란외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로,인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 다이나믹하게 주입
Application파일과 같은 경로에 Config파일을 만든다. 아래와 코드를 통해 MemberService, MemberRepository 클래스를 빈에 등록할 수 있다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
이렇게 따로 config 파일에 빈을 등록하는 경우는 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
현재는 메모리 DB를 사용하고 있는데 나중에 DB를 변경할 경우 Config의 MemoryMemberRepository() 부분만 변경해주면 의존관계를 변경할 수 있다.
package hello.hellospring.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;
}
}
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@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();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
구현한 코드가 잘 동작하는지 테스트하기 위해서는 테스트 코드를 작성해야 한다.
기본적으로 스프링 부트를 활용하면 src/test/java 경로가 만들어져 있어 경로에 테스트 코드를 작성하기만 하면 된다.
-테스트 코드
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.util.List;
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();
//Assertions.assertEquals(member, result);
assertThat(member).isEqualTo(result);
}
@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);
}
}
코드를 실행하면 각각의 메소드가 실행되게 된다. 이때 메소드의 실행 순서는 무작위여서 실행 순서에 따라 테스트가 원래 의도대로 실행되지 않을 수 있다.
예를 들어 findByName이 실행된 이후 findAll 메소드가 실행되면 result.size = 4 이므로 테스트가 실패하게 된다.
이를 방지하기 위해서 @AfterEach를 사용하면 테스트가 종료될 때 마다 이 기능을 실행한다. 이 예제에서는 메모리 DB에 저장된 데이터를 삭제한다.
(테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다!)
회원 서비스
서비스 부분은 비즈니스 로직이 구현되는 부분이다.
-회원 서비스 개발
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
/*
private final MemberRepository memberRepository = new MemoryMemberRepository();
*/
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
회원 가입
같은 이름이 있는 중복 회원X
*/
public Long join(Member member) {
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);
}
}
-회원 서비스 테스트
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 = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
*/
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 중복_회원_예외() {
//ginven
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("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
Dependency Injection
public class MemberService {
/*
private final MemberRepository memberRepository = new MemoryMemberRepository();
*/
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
class MemberServiceTest {
/*
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
*/
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
}
테스트 코드의 주석 처리된 기존 부분의 코드는 memberService의 리포지토리와 memberRepository가 서로 다른 객체였다. 물론 리포지토리 코드의 변수 부분은 static으로 다른 객체이더라도 문제가 없이 동기화 되지만, 이렇게 같은 의미를 가리키면서 다른 객체를 생성하는 것은 좋지 않다.
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
따라서 Dependency Injection(DI)를 통해 같은 memberService의 리포지토리와 생성한 memberRepository가 같게 만드는 것이 좋다.
@BeforeEach는 테스트 메소드가 실행되기 전에 실행되는 부분!
1) 메소드가 실행되기 전에 memberRepository 객체를 1개 만든다.
2) memberService 객체를 1개 만든다.
2-1) memberService 객체의 생성자에 방금 만든 memberRepository를 넘긴다.
--> 이로써 memberService의 리포지토리가 생성한 memberRepository와 일치하게 된다.