
백엔드를 공부를 시작하게 되었다.
그렇기에 대한민국 백엔드 개발자라면 국룰(?)로 해야 하는 스프링을 공부하기로 했다.
Inflearn의 김영한 개발자님의 "스프링 핵심 원리 - 기본편"을 들으면서 복습과 정리를 위해 이 글을 쓰게 되었다.
강의에 들어가면서 가장 강조된 것은 객체 지향 설계였다.
우선 정의는
어떤 프로그램의 일부분에 해당하는 작은 부품, 즉 객체를 먼저 만들고 이렇게 만들어진 여러 객체들을 조립해서 하나의 완성된 프로그램을 만드는 프로그래밍 방법론
이라고 한다.

음 약간 알고리즘으로 치면 분할정복과 같은 개념인 것 같다.
이 객체 지향적인 설계를 할려면 아래의 5가지 원칙들이 중요하다고 로버트 마틴(clean code 저자)가 강조를 했다고 한다.
• SRP: 단일 책임 원칙(single responsibility principle)
• OCP: 개방-폐쇄 원칙 (Open/closed principle)
• LSP: 리스코프 치환 원칙 (Liskov substitution principle)
• ISP: 인터페이스 분리 원칙 (Interface segregation principle)
• DIP: 의존관계 역전 원칙 (Dependency inversion principle)
여기서 DIP와 OCP가 학습 중에 중요성이 강조되서 그 2개를 알아보자

프로그래머는 추상화에 의존해야지 구체와에 의존해서는 안된다.
여기서 추상화와 구체화의 차이를 모르겠다면 추상화는 배역, 구체화는 배우라고 할 수 있다
이것을 자바로 들고 오면 추상화는 interface, 구체화는 Class라고 생각하면 되겠다.
즉 interface, 역할에 접근해야지 직접적으로 class, 역할을 맡고 있는 객체에 접근해서는 안된다는 것이다.
소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
이 말은 확장시킬 수도 있고 변경도 가능하지만 위의 dip와 마찬가지로 객체에 직접 접근을 통한 변경은 안된다라고 나는 이해를 했다.
위의 특징들로 보아서 내가 보기엔 객체 지향 설계는 구조화를 통해서 확실히 영역을 나누고 권한을 제한해서 프로그래밍을 하는 것으로 보인다.
주어진 상황은 회원을 등록하고 찾아야 하는 상황이다.
밑과 같은 다이어그램을 그릴 수 있다.

MemberService -> join, findMember
MemberRepository -> save, findById
와 같은 함수를 가지고 있다.
작동 방식은 밑과 같다.
- MemberService를 상속한 MemberServiceImpl에서
MemberRepository를 상속한 MemoryMemberRepository 호출- MemoryMemberRepository에서 회원 실질적으로 등록하거나 찾음(찾은 경우 반환)
이런 코드들이 잘 되는 지 확인하기 위해 junit을 통해 테스트를 진행할 때 Junit을 사용하면 된다.
Junit은 단위테스트 프레임워크로서 annotation을 통해 간편하게 사용할 수 있다.
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join() {
//given
Member member = new Member(Id, "memberName", Grade.VIP);// 회원 객체 만들기
//when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// serviceImpl에서 join, findMember 요청 -> MemoryMemberRepository에서 처리
//then
Assertions.assertThat(member).isEqualTo(findMember);// 검증
}
}
이 외에도 @BeforeEach(Test 전 기본설정) 등 다양한 annotation을 이용해서 test를 해볼 수 있다는 것이 장점이다.
원래 System.out.println으로만 확인을 했었는데 Assertions를 이용하면 내 눈으로 검증하지 않아도 된다.
그런데 이상하다?
분명 DIP에 따르면 구체화 즉 클래스에 직접적으로 접근하는 것은 객체 지향 설계가 지양하는 것이다.
MemberService memberService = new MemberServiceImpl();//위 test 코드의 일부
하지만 위의 코드에서는 MemberServiceImpl에서는
MemberRepository(interface)를 implements한 MemoryMemberRepository(class)를 호출하면서 interface를에 의존할 뿐만이 아니라 class에도 의존을 한다.

=> 즉 interface를 만들고 그것을 계승한 class를 만들어서 구조는 그럴싸하게 보였지만 완전한 추상화가 아닌 일부 구체화를 하고 있었던 것이다.
즉 직접 객체에 접근하니 DIP, OCP를 지키지 못한 코드인 것이다.
자 만약에 일진 유연석이 여신 문가영에게 빼빼로를 주고 싶은데 직접 주기에는 폼이 망가진다고 하자.
그러면 자신의 졸병인 문태유를 시켜서 빼빼로를 전달하면 그만이다.
즉 직접 객체에 접근하지 말고 다른 얘가 접근하게 하면 되는 것이다.

그 다른 얘가 여기선 AppConfig가 될 것이다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
이런 식으로 AppConfig에서 interface를 반환하는 함수를 만들어 놓으면 TEST 코드는 아래와 같이 변경된다.
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
//given
Member member = new Member(Id, "memberName", Grade.VIP);// 회원 객체 만들기
//when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// serviceImpl에서 join, findMember 요청 -> MemoryMemberRepository에서 처리
//then
Assertions.assertThat(member).isEqualTo(findMember);// 검증
}
}
이러면 AppConfig가 interface를 만들면서 AppConfig가 MemoryMemberRepository를 접근하게 되는 것이기에 AppConfig를 제외하고는 누구도 직접적으로 객체에 접근할 수 없다.
그렇기에 AppConfig는 일종의 권한을 가진 관리자라고 볼 수 있는 것이다.
뭐 아무도 접근 못하는 건 사용을 하지 말라는 것이니 이렇게 해서 개나 소나 다 접근하기보다는 관리자만 접근하게 만들어 DIP, OCP를 지켰다고 볼 수 있다.
그리고 이렇게 하는 것의 장점은 객체를 변경할 때, AppConfig에서만 수정하면 되기에 수정도 간편해진다.

즉 클라이언트 입장에서는 의존관계를 외부(AppConfig)에서 대신 주입해주는 것이나 마찬가지기에 Dependenncy Injection, 즉 DI라고 한다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
위에서 이러한 형태로 AppConfig를 작성을 했는데 여기서 역할이 좀 더 구분되게 수정을 한다면
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository()l
}
}
이런 식으로 수정이 가능하다.
이로 인해 애플리케이션의 구성을 한 눈에 볼 수 있으며 혹시 MemoryMemberRepository를 다른 곳들에서 중복해서 사용한다고 해서 MemberRepository에서 수정하면 되니 역할에 따른 구현이 잘보이게 된다.
우선 나의 이해 상태를 기반으로 쓴 글이기에 고칠 점들이 있다면 언제나 환영이다.
사실 수강러쉬를 실패해 대학교에서 객체지향 강의를 놓쳤었는데 이렇게 배워보니 코딩이 이렇게 체계적일 수 있구나를 느끼게 된다.
아무생각없이 백준 막 풀던 때와 정반대의 느낌이라서 참 흥미롭다.
암튼 앞으로도 꾸준히 공부해야겠다.
(참고로 유연석, 문가영 예시는 요즘 드라마 "사랑의 이해"를 보고 있어서.. 큼흠)