백엔드 데브코스 TIL 4기 - 06.05(월)

신재윤·2023년 6월 5일
0
post-thumbnail

💻 2주차 DAY 01

2주차 DAY 01부터 본격적으로 과제가 나오며 바쁜 일상이 시작되겠다는 것이 느껴졌다. 아직 부산에 방이 빠지지 않고 서울에 방을 구하지 못한 상황이고, 개인사로 인하여 심리적으로 안정되지 못한 상태이기 때문에, 공부에 풀 집중이 안되는 것이 아쉽다. 하지만, 현재 상황에 맞춰서 최대한 집중해보려고 한다 :)


객체지향 프로그래밍

평소, JAVA를 사용해왔기에 나는 객체지향 프로그래밍을 한다고 생각했지만, 오늘 강의를 통해 스스로 의문을 가지게 되었다. 나는 과연 객체지향적인 특성을 잘 활용하며 코드를 짜고 있을까? 결국 객체지향 프로그래밍을 한다는 것은 좋은 객체를 잘 만들고 있는가를 의미한다고 생각했다. 프로그램이 거대화하면서 등장하게 된 객체지향 프로그래밍이라는 패러다임에 따른다면, 좋은 객체와 나쁜 객체는 아래와 같다.

  • 좋은 객체 : 역할과 책임에 충실하면서 다른 객체와 잘 협력하여 동작하는 객체
  • 나쁜 객체 : 여러가지 역할을 한 가지 객체에게 부여하거나, 이름과는 맞지 않는 속성과 기능을 가지도록 하거나, 제대로 동작하지 않는 객체. 또한 다른 객체와 동작이 매끄럽지 않는 것도 나쁜 객체

POJO와 PSA

좋은 객체를 생각하며, 자연스럽게 POJO(Plain Old Java Object)가 떠올랐다. POJO에 관한 자세한 내용은 Dev Uni 기록용 블로그 - POJO 설명 을 참고하자.

POJO(Plain Old Java Object)는 직역하자면 오래된 방식의 간단한 자바 객체라는 의미이다. 이는 특정 기술에 종속되지 않는 순수한 자바 객체를 의미한다.

객체 지향적인 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 POJO라 말하고 POJO에 애플리케이션의 핵심로직과 기능을 담아 설계하고 개발하는 방법을 POJO 프로그래밍이라고 할 수 있다.

예를 들어, ORM(Object Relationship Mapping) 기술을 사용하려면 ORM을 지원하는 프레임워크를 사용해야한다. 만약에 Java 객체가 ORM 기술을 사용하기 위해 Hibernate 프레임워크를 직접 의존하는 순간 POJO라고 할 수 없다. “특정 기술에 종속되었기 때문"이다.

그런데 Java 객체가 ORM 기술을 사용하기 위해 Hibernate 프레임워크를 직접 의존하는 순간 POJO가 아니라고 했으면서, Spring은 어떻게 POJO를 유지하면서 Hibernate를 사용할까? 이는 Spring에서 정한 표준 인터페이스가 있기 때문이다.

ORM을 사용하기 위해 JPA(Java Persistence API)라는 표준 인터페이스를 정의해뒀고, ORM 프레임워크들은 JPA의 구현체가 되어 실행된다. 이것이 새로운 엔터프라이즈 기술을 도입하면서도 POJO를 유지하는 방법이고 이런 방법을 PSA라고 한다.

PSA(Portable Service Abstraction)란 환경과 세부 기술의 변화에 관계없이 일관된 방식으로 기술에 접근할 수 있게 해주는 것을 의미한다. PSA가 적용된 대표적인 예시는 JDBC, JPA, Transaction Manager가 있다.

다시 POJO로 돌아와서, 그렇다면 특정 기술규약과 환경에 종속되지 않으면 모두 POJO라고 말할 수 있을까? 그렇지 않다. 진정한 POJO는 위에서 말한것처럼 객체 지향적인 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 의미한다.

정리하자면, 객체지향적인 원리에 충실하는 좋은 객체이면서 환경과 기술에 종속되지 않고 필요에 따라 재활용 될 수 있는 방식으로 설계된 오브젝트가 POJO이구나 !


아키텍처 이야기

거대한 프로그램에서 객체가 역할과 책임에 충실하도록 기능을 나눠서 좋은 객체로 만드는 것을 살펴보았다. 그렇다면 이것을 아키텍처 자체의 관점에서 살펴보자. 내가 지금까지 코드를 짜면서 해왔던 모든 방식은 모놀리식 아키텍처이다. 모놀리식 아키텍처는 전통의 아키텍처를 지칭하며, 소프트웨어의 모든 구성요소가 한 프로젝트에 통합되어 있는 형태이다.

최선을 다하여 좋은 객체를 만들고 잘게 쪼갰음에도 애플리케이션의 한 프로세스에 대한 수요가 급증하면 아키텍처 전체를 확장해야하는 상황이 왔다고 하자. 모놀리식이니까 scale up 형식으로 서버 스펙을 향상 시키는 것도 극한에 이르렀다고 가정하자. 어떻게 더 쪼개야할까?


  1. MSA (Micro Service Architecture)

아마 먼저 떠올린 생각이 아닐까싶다. 모놀리식 아키텍처에서 MSA 아키텍처로 변경하는 것을 생각할 수 있다. 그러나, MSA 아키텍처에 관하여 자세히는 모르지만, 현직에 계신 분들의 의견을 들었을 때 생각보다 MSA에 부정적이신 분들이 많았고 MSA로 쪼개는 것은 그렇게 어울리는 방법이 아닐 수도 있다. 대표적인 단점은 아래와 같다.

  • 서비스가 분산되어 있어 관리하기 어려움
  • 트랜잭션 관리, 장애 추적 및 테스트 등이 쉽지 않음
  • 서비스마다 DB가 분리되어 데이터의 조회가 어려움 (당연히 DB 샤딩의 난이도도 급증할 것) + 데이터의 중복 발생
  • 테스트가 불편
  • 서비스 간 호출 시 REST API 사용으로 인한 통신비용, Latency(지연시간) 증가
  • 전체 서비스가 커짐에 따라 복잡도가 기하급수적으로 높아질 수 있음.

이 중 가장 큰 단점은 개인적인 생각으로 비용적인 측면에 있지 않나라고 생각한다. DAU가 높지 않은 상황에 무리하게 모놀리식에서 MSA로 전환한다면 비용적인 폭탄을 맞지 않을까... 현업에 발을 디뎌본 적도 없는 취준생이라 개인적인 추측이고 자세한 상황은 모르겠지만 MSA로 전환은 쉽지 않다고 생각한다.


  1. 모놀리식을 유지하면서 멀티모듈

최초에 애플리케이션을 개발할 때 멀티모듈로 개발한다면, 추후 서비스가 커졌을 때 확장성도 좋고 응집도는 커지되, 결합도는 낮아진다라는 이야기를 들었다. 아직, 멀티 모듈에 관하여 지식이 부족한 상태라 장단점을 확실하게 언급할 수는 없지만, 내가 채택을 해야하는 상황이라면 MSA 보다는 이 방식을 채택할 것 같다. 멀티모듈에 관한 자세한 이야기는 우아한 형제들 - 멀티모듈 설계 이야기 를 참고하자.


객체지향의 특성

좋은 객체, POJO가 되기 위하여 객체지향적인 원리에 충실해보자. 그러기 위하여 객체지향의 특성을 알아보려고 한다. 사실 개발 공부를 하면서 이런식의 암기는 좋아하지 않는데, 저번에 인터넷에서 보고 바로 외워버려서 말해보려고 한다. 객체지향의 특성은 캡상추다! (캡슐화, 상속, 추상화, 다형성)으로 쉽게 외워보자 ㅋㅋ


1. 캡슐화

  • 완성도가 있다.
  • 정보가 은닉되어있다.

좋은 객체가 되려면 객체가 기능을 수행해야 하는 책임을 가진다고 했는데, 이러한 관점에서 완성도가 있다는 의미이다. 캡슐화를 진행하면서 의존성을 낮추고 결합도를 낮추는 것이 핵심이다.

캡슐화 과정에서 접근지정자, 접근제한자를 통해 정보은닉이 자연스레 따라온다. 객체의 정보에 밖에서 접근하는 행위를 막는 것이다.

디자인패턴의 가장 중요한 원칙과도 일맥상통한다. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리하여 나머지 코드에 영향을 주지 않도록 캡슐화하는 점을 의미한다.


2. 상속

  • 상위 = 부모 = super = 추상 객체
  • 하위 = 자식 = (this) = 구체 객체

곰튀김님께서 상속에 관한 오해를 설명해주셨을 때 가슴이 뜨끔했다. 평소 내가 가지던 생각과 완벽하게 일치했기 때문이다. 맹목적으로 공통된 기능이 있다면 상속으로 빼려고 한 안일한 생각이었다.

  • 오해 : 공통된 기능을 여러 객체에게 전달하고 싶을 때 상속을 많이 사용
  • 진실 : 추상과 구체 관계에서 상속을 표현해야 한다.

3. 추상화

  • 상속에서의 진실은 자연스럽게 추상화의 개념으로 이어진다.
  • 상속관계는 상위로 갈수록 추상화, 하위로 갈수록 구체화 된 모습이다.
  • 추상체 : 추상화된 객체 / 구상체 : 구체적인 객체
  • 객체 간 관계에서 상위에 있는 것이 항상 하위보다 추상적이어야 한다.

추상체와 구상체를 만드는 기준에는 크게 3가지 정도가 있다.

  • 둘다 구체적인 기능을 가지고 있으며, 의미적으로만 묶은 상태
  • 구현체를 가지지 않고 정의만 하여 추상클래스 형태로 제공하는 상태
  • 객체 자체가 추상적인 인터페이스 상태

여기에서, 왜 추상체를 가져야할까?에 관한 이유는 자연스럽게 다형성과 연결된다.


4. 다형성

  • type(형)을 여러 형태로 표현할 수 있다.
  • 즉, 구체 클래스가 아닌 추상 클래스로 표현할 수 있는 상태를 의미한다.
  • 누가 접근하느냐에 따라 필터링 된 기능만 제공하는 형식도 가능하다.
List<Integer> list = new ArrayList<Integer>();

구체 클래스인 ArrayList를 추상 클래스인 List로 표현할 수 있는 상태이면서, List에 해당하는 기능만 필터링 된 형식으로 제공한다. 즉, ArrayList에만 있는 기능은 위와 같은 코드에서 사용할 수 없다.

다른 예시로는 아래와 같은 로그인이 있다.

구상체인 카카오 로그인이나 네이버 로그인으로 표현하지 않고 추상체인 로그인 인터페이스로 표현하면서, 접근하는 사람에 따라 필터링 된 기능만 제공하는 형식이 가능하다.


객체지향 설계, SOLID

캡슐, 상속, 추상화, 다형성 같은 객체지향의 특성을 잘 지키는 좋은 객체가 의미하는 바는 알았다. 그렇다면 좋은 설계를 했는지는 어떻게 알아야할까? 이에 대한 답은 아래 2가지이다.

  • 객체를 어떻게 구분했는지
  • 객체간의 연관관계가 어떠한지

좋은 설계를 하기 위해 객체를 기능에 따라 어떻게 잘 나누고, 어떻게 잘 연관짓느냐에 관한 원칙이 SOLID 원칙이다.


  • S : SRP (Single responsibility principle)
    - 단일 책임 원칙
    - 수정이 필요할 경우, 수정되는 이유는 하나 때문이어야 한다.
    - 객체를 하나 만들면, 그 객체에다가 책임을 하나만 줘라
    - 책임은 객체가 수행해야 하는 임무, 기능을 의미
    - 그 객체가 가진 기능은 하나만 있으면 된다.
    - 하나의 객체에 너무 많은 기능을 가지게 하지마라

  • O : OCP (Open/Closed principle)
    - 개방 폐쇄 원칙
    - 수정에는 닫히고, 확장에는 열어라
    - 처음부터 객체를 만들 때, 수정할 수 있는 형태로 만드는 것이 아니라, 확장해서 기능을 변경할 수 있는 방식으로 만들어라.
    - 즉, 기존거 건들지마라

  • L : LSP (Liskov substitution principle)
    - 리스코프 치환 원칙
    - 추상객체로 사용되는 부분에 구상객체가 들어가도 아무 문제 없어야 한다.
    - 추상체와 구상체의 관계가 명확하다면, LSP는 잘 지켜질 것 이지만, 추상화에서 언급한 상속관계에서 공통적인 기능은 부모에 두는 경우로써 상속을 사용했다면 LSP가 깨지기 쉽다.

  • I : ISP (Interface segregation principle)
    - 인터페이스 분리 원칙
    - 로그인이라는 인터페이스가 있고 검색이라는 인터페이스로 나눠서 구성한다고 하자. 로그인 기능만 제공해주고 싶으면 로그인 인터페이스로 보낼 수 있고, 검색 기능만 제공해주고 싶으면 검색 인터페이스로 보낼 수 있는 것이니까 인터페이스를 분리하면 유리하다.

  • D : DIP (Dependency inversion principle)
    - 의존관계 역전 원칙
    - 상위 레벨의 모듈은 절대 하위 레벨 모듈에 의존하지 않는다. ( = 둘 다 추상화에 의존해야 한다 )
    - 결국, 클래스 간 결합을 느슨히 하기 위함이다. 클래스의 변경에 따른 클래스들의 영향을 최소화하는 것이 목적
profile
백엔드 데브코스 TIL (Today I Learned)

0개의 댓글