Blog
강의 리뷰
실무 자바 개발을 위한 OOP와 핵심 디자인 패턴

실무 자바 개발을 위한 OOP와 핵심 디자인 패턴

이 게시물은 Programmers 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 (opens in a new tab)

01 자바와 객체지향

객체지향적으로 개발해야 하는 이유

  • 캡슐화 : 응집도를 높이게 된다.

아래 코드를 보면 2번 코드는 데이터와 로직이 응집도가 높다.

class Product {
    public String name;
    public Integer price;
    public Integer amount;
}
 
class ProductFunctions {
    public static Integer getTotalAmout(Product product) {
        return product.price * product.amount;
    }
}
 
class SomeClass {
    public void someMethod(Product product) {
        //어떤 로직들
 
        Integer totalAmount = ProductFunctions.getTotalAmout(product);
    }
 
    public void anotherMethod(Product product) {
        // 어떤 로직들 
        
        Integer totalAmount = ProductFunctions.getTotalAmout(product);
        
        // 어떤 로직들 
    }
}

인터페이스

  • 두 개의 인터페이스를 상속했어도
public class Main {
    public static void main(String[] args) throws InterruptedException {
        SomeInterface some = new ImplementsClass();
        AnotherInterface another = new ImplementsClass();
        some.anotherMethod(); // 실행 불가
        another.someMethod(); // 실행 불가
    }
}
interface SomeInterface {
    void someMethod();
}
interface AnotherInterface{
    void anotherMethod();
}
class ImplementsClass implements SomeInterface, AnotherInterface {
 
    @Override
    public void someMethod() {
        System.out.println("call some method");
    }
 
    @Override
    public void anotherMethod() {
        System.out.println("call another method");
    }
}

default 메서드

인터페이스에 메서드를 정의할 수 있는 기능이다. 이미 정의되지 않은 메서드를 호출하는 것이 가능하다.

public interface SomeInterface {
  void someMethod();
 
  default void defaultMethod() {
    // 인터페이스에 메서드 정의 가능
    this.someMethod();
  }
}

인터페이스 대신 추상 클래스를 사용하는 경우

  1. 인스턴스 변수가 필요한 경우
  2. 생성자가 필요한 경우
  3. Object 클래스의 메서드를 오버라이딩 하고 싶은 경우

Checked Exception vs Unchecked Exception

  • Checked Exception : 예외처리를 컴파일 전에 강제한다. Exception을 상속받은 Exception을 Checked Exception이라고 한다.
    • Checked Exception을 사용할 때 Exception이 발생할 수 있다는 것을 외부에 알림으로써 캡슐화를 깨게 될 수 있다.
  • Unchecked Exception : 예외처리를 컴파일 전에 강제하지 않는다. RuntimeException을 상속받은 Exception을 Checked Exception이라고 한다.
    • Unchecked Exception을 사용하더라도 Try Catch가 사용 가능하다.
👉 강사님 의견(결론)

Checked Exception보단 Unchecked Exception을 주로 사용하자.

Object

동일성 동등성
  • 동일성 : 완전히 같다. 주소가 같고 완전히 동일한 인스턴스이다. (== 사용)
  • 동등성 : 격이 같다. (equals 사용)
    • 오브젝트 간의 동등성 : 두 객체가 다른 오브젝트라도 변수 등 내용이 같다.
  • 동일한 것은 반드시 동등하지만 동등한 것이 반드시 동일한 것은 아니다.
public class Main {
    public static void main(String[] args) throws InterruptedException {
        SomeObject some1 = new SomeObject("hi");
        SomeObject some2 = some1;
        SomeObject some3 = new SomeObject("hi");
 
        System.out.println(some1 == some3); // false
        System.out.println(some1.equals(some3)); // true
 
        System.out.println(some1 == some2); // true
        System.out.println(some1.equals(some2)); // true
    }
}
class SomeObject {
    public String value;
    public SomeObject(String value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SomeObject that = (SomeObject) o;
        return Objects.equals(value, that.value);
    }
    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
equals와 hashCode를 같이 오버라이드 해줘야 하는 이유
public class Main {
    public static void main(String[] args) throws InterruptedException {
        SomeObject some1 = new SomeObject("hi");
        SomeObject some2 = new SomeObject("hi");
 
        Set<SomeObject> set = new HashSet<>();
        set.add(some1);
        set.add(some2);
 
        System.out.println(set.size()); // 1
    }
}
class SomeObject {
    public String value;
    public SomeObject(String value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SomeObject that = (SomeObject) o;
        return Objects.equals(value, that.value);
    }
    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

위의 코드를 보면 우리는 hashCode를 value를 기준으로 생성해주고 있다.
Set 또는 HashSet에서 데이터를 넣을 때 hashCode를 비교(==연산) 하고 equals를 비교해준 후 중복인지 아닌지를 판단해주는데 만약 equals나 hashCode 둘 중 하나라도 override하지 않았다면 중복 처리가 제대로 되지 않을 수 있다.
정말로 그렇게 동작하는지 궁금하다면 위의 코드에서 equals나 hashCode 둘 중 하나를 주석처리하고 실행해보면 실행결과가 2가 나오는 것을 알 수 있다.

02 객체지향의 핵심 원리와 원칙들

의존 관계가 생기는 경우

  • 클래스 또는 인터페이스의 레퍼런스 변수 사용
  • 클래스의 내부에서 다른 클래스의 인스턴스를 생성
  • 클래스 또는 인터페이스를 상속받는 경우

의존성역전

고수준 컴포넌트가 저수준 컴포넌트에 의존하지 않도록 의존 관계를 역전시키는 것

  • 고수준 컴포넌트 : 기술(프레임워크 등)에 종속적이지 않은 코드
  • 저수준 컴포넌트 : 기술(프레임워크 등)에 종속적인 코드

아래 사진을 보면 의존성 역전이 일어난 부분은 서비스에서 레포지토리 부분이 아니라 레포지토리 인터페이스에서 세부 레포지토리의 의존성 방향이 역전이 되었다. 의존성 역전이 중요한 이유는 고수준 컴포넌트인 서비스 레이어가 저수준 컴포넌트의 변경에 의해서 변경이 일어나지 않고 저수준 컴포넌트의 값들이 고수준 컴포넌트에 맞추어서 동작하도록 해줄 수 있기 때문이다.


👇


의존성 주입

  • Bean
    • 스프링의 IoC 컨테이너가 주입해주는 의존성
    • 싱글톤으로 관리가 됨

객체지향 4대 원칙

  • 캡슐화 : 객체의 상태를 외부로부터 보호하는 것
  • 상속 : 부모 클래스의 특성을 자식 클래스가 물려받는 것
    • 다른 클래스의 메서드를 재사용하기 위해서 상속을 하면 안됨
    • 필드를 확장하기 위해서 확장해야 함
  • 다형성 : 하나의 인터페이스를 여러 형태로 구현할 수 있는 것(똑같은 클라이언트 코드로 안에 들어있는 존재에 따라 다른 동작이 수행되는 것)
  • 추상화 : 실제 세상에 있는 대상을 추상적인 자료형으로 표현하는 것

SOLID

SRP

Single Responsibility Principle : 하나의 클래스는 하나의 책임만을 가져야 한다. 하나의 클래스에는 변경하려는 이유가 하나여야 한다.

  • 책임 : 변경하려는 이유
SRP 위반 유형 1

만약에 Service에서 HTTP Status Code를 발송해주고 있는데 Controller가 HTTP가 아닌 Message Queue로 통신하는 방식으로 변경이 된다면, Service의 Exception도 변경을 해줘야하고 이는 Controller의 요구사항 변경이 Service의 요구사항 변경으로 이어지게 되는 것이다.

SRP 위반 유형 2

위 코드와 같이 각각의 Repository에서 가져온 데이터로 동작하는 코드가 하나의 Service에 작성이 되어 있다면 이는 SRP를 위반하는 것이다. 하지만 두 개의 Repository에서 가져온 데이터를 조합해서 응집도있게 동작한다면 이는 SRP를 위반한 코드라고 할 수 없다.

결론

단일 책임 원칙을 지키는 코드는 각각의 클래스가 응집력이 높기 때문에 코드의 재사용성이 높아지고, 캡슐화를 통해 한 클래스의 변경이 다른 클래스에 영향을 미치지 않도록 한다.

OCP

Open Closed Principle : 확장에는 열려있고 수정에는 닫혀있어야 한다.

예를 들어서 Repository를 들 수 있는데 Repository Interface를 통해서 구현체를 만들어서 사용하고 Repository Interface를 주입받는 형태라면, 다른 종류의 Repository를 주입받아 사용하는건 쉽고 확장이 용이하며 다른 레포지토리의 동작을 수정하게 되더라도 다른 코드에 영향을 주지 않기때문에(인터페이스만 지킨다면) OCP를 지킨다고 할 수 있다.

LSP

Liskov Substitution Principle : 파생 클래스는 기반 클래스를 대체할 수 있어야 한다. (자식 클래스는 부모 클래스를 대체할 수 있어야 한다.)

부모가 하는 일을 자식이 못하는 상황

부모의 코드는 어떤 int 값이 들어도 상관이 없는데 자식의 코드는 0이하가 들어올 경우 Exception을 발생하는 상황이다.


위 코드를 고치기 위해서 다음과 같은 예를 들 수 있다.

  1. 애초에 Parent대신 Child를 사용하게 만들어서 부모의 코드를 사용하지 않게 만든다.

하지만 이러면 Parent를 상속받은 다른 자식을 사용할 수 없기 때문에 다형성이 지켜졌다고 보기 힘들다.

  1. client 코드 내에서 Parent를 분기처리해서 사용한다.

애초에 다형성이라는 개념이 생기게된 이유로써 하나의 메서드가 다른 두 역할을 하게 해주는 코드를 직접 코딩한다면 이는 다형성이 깨진 코드라고 할 수 있다.

ℹ️

instanceof는 부모 클래스를 넣어서 자식 클래스와 비교했을 때 true를 반환한다.
예를 들자면 Parent의 인스턴스와 Parent의 자식 클래스인 Child를 instanceof로 비교하면 true를 반환한다.

계약에 의한 설계

사전 조건은 자식 클래스에서 더 강해지면 안된다.(파라미터에 들어올 수 있는 값의 범위가 더 좁아지면 안된다.)

결론

리스코프 치환 원칙은 진정한 의미에서의 자형성을 제공해주기 때문에 코드의 분기 없이도 올바르게 확장 가능한 코드를 만든다.

ISP

SRP는 변경의 이유였던 책임을 하나로 만들어야 한다는 원칙인 반면에 인터페이스 분리 원칙은 불필요한 인터페이스의 노출을 막아야 한다는 원칙이다.

DIP

Dependency Inversion Principle : 고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.

의존역전 원칙이 깨지는 상황

위와 같이 Repository가 각각 다른 Exception을 던지고 있다면 Service에서는 각각 다른 Exception을 처리해줘야 한다. 이는 의존성 역전 원칙이 깨진 코드라고 할 수 있다.

03 디자인 패턴

옵저버 패턴

어떤 대상의 상태 변화에 관심 있어야하는 대상들에게 상태가 변화됐다고 전파할 수 있는 패턴 (어떤 일에 대한 구독과 발행 )