의존성이 잘못 관리될 경우, 변경하기 어렵고 재사용하기 어려운 코드가 작성이 된다.
그렇기에 우리는 의존성을 잘 관리된 코드를 작성하려 노력해야 한다.
그렇다면 의존성을 잘 관리하기 위한 기준은 무엇이 있을까?
바로 SOLID 원칙이 있다.
정처기 시험을 볼 때 꽤 자주 나왔지만, 말 그대로 정의만 있을 뿐 이를 단순히 정의만 보면 이해 하기가 어려우니 예시와 코드를 보며 이해해보자.
++ DIP(의존 역전 원칙)은 의존 관계를 설명하며 설명했기에 패스하겠다.
- SOLID
- SRP : Single Responsibilirty Principle
- OCP : Open Closed Principle
- LSP : Lskov Substitution Principle
- ISP : Interface Segregation Principle
- DIP : Dependency Inversion Principle
* SRP(Single Responsibility Principle)
SRP 원칙이 깨져있는 코드를 예시로 이해해보자.
public class service {
private MemberRepository memberRepository;
private CompanyRepository companyRepository;
public void createMember(...){...}
public Member findMember(...){...}
public void createCompany(...){...}
public Company findCompany(...){...}
}
지금 서비스 클래스 하나에 userRepository에 해당하는 메서드와 companyRepository에 해당하는 메서드들이 있다.
만약 repository들이 증가하면 해당 함수들도 계속 증가한다.
이는 문제가 있다.
그러므로 클래스를 분리하는 것이 맞다.
클래스를 분리했다 가정해보자.
public class MemberService {
private MemberRepository memberRepository;
public void createMember(...){...}
public Member findMember(...){...}
}
public class CompanyService{
private CompanyRepository companyRepository;
public void createCompany(...){...}
public Company findCompany(...){...}
}
만약 그렇다면 재사용성이 높아진다.
컨트롤러가 여러 개가 될 시 각자가 필요한 서비스를 의존 주입 받아서 사용하면 되니까!!
다만 주의할 점은, 단순히 서비스가 하나의 repository만을 가져야한다고 오해하지는 말자.
public class MemberService {
private MemberRepository memberRepository;
private CompanyRepository companyRepository;
public Member findMemberByCompany(String companyId){
Company company = companyRepository.findByCompanyId(companyId);
Member member = memberRepository.findByCompany(company);
}
}
이런 식으로 repository가 여러 개 필요한 함수들이 존재할 수도 있다.
즉 SRP 원칙, 단일 책임 원칙을 지키는 코드는 각각의 클래스가 응집력이 높기 때문에
코드의 재사용성이 높아지고, 캡슐화를 통해 한 클래스의 변경이 다른 클래스에 영향을 미치지 않도록 만들어준다!
* OCP (Open Close Principle)
개방 폐쇄 원칙의 핵심은 확장은 자유롭게, 변경은 폐쇄적으로 한다이다.
현재 예를 들어, Repository는 인터페이스로 구현이 되어있다.
내가 예를 들어 해당 인터페이스의 구현체를 추가한다해서 서비스나 repository 단에서 추가적으로 코드를 작성할 필요는 없다.
또한, 내가 해당 구현체를 변경한다해서, 인터페이스나 서비스의 코드에 영향을 주지 않는다.
즉, 개방 폐쇄 원칙을 지키는 코드는 클라이언트 코드가 추상화에 의존하고 있기 때문에 (여기서는 인터페이스)
확장될 때와 변경될 때 모두 다른 코드에 영향을 주지 않게 만든다.
* LSP (Liscov Substitution Principle)
이게 무슨 소리일까? 코드로 이해해보자.
public class Parent {
public void calculate(int num){
System.out.println("부모 호출 완료!!");
}
}
public class Child extends Parent{
@Override
public void calculate(int num) {
if(num > 100){
throw new ArrayIndexOutOfBoundsException("숫자 100보다 크면 범위를 넘습니다!!");
}
System.out.println("Child 호출 완료!!");
}
}
현재 Child가 Parent 클래스를 상속해서 오버라이딩했다.
그럼 내가 메인함수에서 이를 호출했다 해보자.
public class Client{
public void doClientMethod(Parent parentOrChild){
parentOrChild.calculate(10000);
}
}
public class ExeMain{
public static void main(String[] args) {
Client client = new Client();
Child child = new Child();
client.doClientMethod(child);
}
}
Client 함수 입장에서는 parentOrChild 변수에 parent가 들어갔는지, child가 들어가있는지 모른다.
현재의 경우, child를 주었기에 예외를 던질 것이다.
그렇다면 이 상황이 현재 우리가 원하는 상황인가? 아닐 것이다.
나는 child 클래스가 parent 클래스가 하는 역할은 다 하길 원할 것이다.
지금 상황은 child는 parent가 할 수 있는 함수를 하지 못하고 있다.
해결 방법은 간단하다. 이와 같은 상황에서는 상속해서는 안된다.
사전 조건은 자식 클래스에서 더 강해지면 안된다.
이는 자바 문법의 접근 제어자를 통해서도 확인할 수 있다.
public class Parent {
public void calculate(int num){ // public
System.out.println("부모 클래스");
}
}
public class Child extends Parent{
@Override
private void calculate(int num) { // private
System.out.println("자식 클래스!");
}
}
자식 클래스가 부모 클래스의 함수를 오버라이딩하면서, 이를 private로 변경했다 가정해보자. 어떻게 될까??
attempting to assign weaker access privileges하라고 한다.
왜 그럴까?
현재 자식 클래스가 private이기 때문에 자식 인스턴스를 만들어서 해당 함수를 호출할 수 없다.
그렇다는 이야기는 자식 클래스가 부모 클래스가 할 수 있는 역할을 하지 못한다는 의미이다.
부모 클래스가 할 수 있는 행동은 자식 클래스도 할 수 있어야 한다.
* ISP (Interface Segregation Principle)
클라이언트별로 세분화된 인터페이스를 만들어야 한다.
public interface Repository {
void createDog();
Dog findDogById(Long id);
void createCat();
Cat findCatById(Long id);
}
public class DogRepository implements Repository{
@Override
public void createDog() {
}
@Override
public Dog findDogById(Long id) {
return null;
}
@Override
public void createCat() {
}
@Override
public Cat findCatById(Long id) {
return null;
}
}
public class CatRepository implements Repository{
@Override
public void createDog() {
}
@Override
public Dog findDogById(Long id) {
return null;
}
@Override
public void createCat() {
}
@Override
public Cat findCatById(Long id) {
return null;
}
}
지금 Repository에 implements 구현한 Dog와 Cat이 있다 해보자.
Dog에는 필요없는 Cat 함수들이 있고, Cat에는 필요없는 Dog함수들을 구현해야 한다.
그렇기에 인터페이스를 분리해야한다.
인터페이스 분리 원칙을 지키는 코드는 구현 클래스에는 불필요한 메서드를 구현하지 않도록 만들고,
인터페이스를 사용하는 클래스에게는 불필요한 메서드를 노출하지 않아 유지보수하기 좋은 코드를 만들어준다.
'Java' 카테고리의 다른 글
collect(Collectors.toList()) vs Stream.toList() (0) | 2023.11.27 |
---|---|
Try with resources (0) | 2023.10.16 |
If문 제거하기 (리펙토링) (0) | 2023.09.21 |
Stream (0) | 2023.09.03 |