개발과 설계와 OOP 원칙

4 minute read

요즘 프로젝트를 진행하면서 java, nodejs, php를 같이 보게 됐다. java는 오랜만에 해보고, nodejs는 처음 해보고, php는 절차지향적으로 개발이 되어 있다

주로 액세스 토큰을 관리하며 API 호출하는 로직을 개발하고 있는데, 각 언어마다 느낌이 달라서 경험하는 김에 주저리 주저리 뭐라도 적어 놔야 할 거 같다.

바로 생각나는 점들은

  • OOP를 하려면 java, typescript 등 타입 명시되는 언어로 개발하는 게 좋다. 비슷한 메서드를 내가 못 찾고 IDE도 못 찾는다
  • 쉬운 걸 어렵게 가려고 하지 말고 어려운 걸 쉽게 가려고 하지 말자. 자바스크립트로 OOP와 디자인 패턴 적용해서 스프링처럼 개발하려다 망했다. 프레임워크가 프레임워크인 이유가 있다. 닭 잡는 데 소 잡는 칼 쓰지 말자. 심플하게 개발할 수 있다면 그게 최고인 거 같다.
  • 추상화는 어렵다. 추상화를 위해서는 현재 서비스와 미래 서비스, 현재 개발 내용과 미래 개발할 수도 있는 내용 등을 모두 알아야 한다. 알면 좋다가 아니다. 진짜 다 알아야 확장성 있게 유연한데 견고하고 퍼포먼스에 지장 없는 추상화가 가능하다.
  • 가급적… 동적 타이핑 언어는 쓰지 말자. 변수 이름이 조금만 비슷해도 미로에 갇히게 된다. 1. 복잡한 서비스에서 모든 변수 패턴을 기억하거나 2. 모든 변수명을 외울 수 있거나 3. 작명 센스가 탁월하거나 4. 인스턴스가 여기저기 여러 곳으로 전달되어 사용되지 않고 한 스크립트 안에서만 생존하고 끝나서 굳이 타입 따위 필요 없는 게 아니라면 정적 타이핑 언어가 좋은 거 같다.

Java

스프링 소셜을 클론해서 OAuth2.0 사양에 맞게 동작하는 API 웹앱을 구현했을 때는 왜 그렇게 복잡하게 만드는지 이해가 안 갔다.

하지만 오랜만에 다시 보니 그때와는 느낌이 다르다. SOLID 객체지향 설계 중 단일 책임 원칙(Single Responsibility Principle)리스코프 치환 원칙(Likov Susbtitution Principle)은 다시 생각하게 된 거 같다.

대략적으로는 세 개의 패키지로 나뉠 거 같다

com.service.sub
  - api
  - oauth
  - service
  main.java

api는 API 호출만 담당하고, oauth는 OAuth만 담당하고, service는 서비스를 담당한다. 단일 책임 원칙을 맞추기 위한 것인 동시에, 개발을 하면서 이렇게 개발이 되어야 한다는 생각도 들었다.

왜일까? class는 사전적으로 수업, 계급, 계층, 종류, 부류 등을 의미한다. 의미를 고려하면 어떤 그룹화를 의미한다. 그리고 이 클래스를 통해 인스턴스를 만들어 낸다.

인스턴스는 실제 어떤 역할(method)들을 수행해야 하는데, 자동차에 발이 달려 있다면 뭔가가 잘못된 것이다. 핸들과 바퀴로 주차하는 것은 귀찮다. 그러니 “그냥 자동차가 주차 자리로 걸어가게 하면 되지 않을까?” 같은 개발 편의적인 생각의 결과일 가능성이 높다.

개발에 불가능한 건 내가 못해서 그렇지 아마도 뭐 거의 없을 테니, 자동차 클래스에 발을 넣어둘 수는 있다. 그리고 뭐 언젠가 미래에는 발이 달린 자동차도 생길 수 있지 않을까? 하지만 service는 당장의 현실이다. 결국 애플리케이션은 !!!개발한대로만!!! 작동한다. 현실 서비스에 발이 달린 자동차가 섞이면 그때부터는 현실 논리가 아닌 클래스 설계대로 작동하게 된다.

그럼 진짜 어느 순간에는 적어도 해당 애플리케이션 상에서는 자동차가 주차 자리로 걸어가는 게 당연하게 되고 $\to$ 어느 순간부터는 바퀴와 핸들이 의미가 없어지고 $\to$ 바퀴와 핸들로 작동하는 자동차만 존재하는 현실에 기반해 확장하는 서비스에 대응할 수 없게 되며 $\to$ 이게 바로 기술 부채가 되고, $\to$ 누가 뭔가 당연히 되는 걸 물어봤을 때 개발자가 깊게 고민하게 된다.

자동차에 발이 달렸는데 가령 서비스에 방지턱 관련 기능 추가가 필요해서 “방지턱 높이만큼 자동차 하단에 아무것도 없게 해주세요”라는 요구를 들었을 때, 과연 될까? 그때는 개발자 머릿속에 “서비스 로직에서 자동차가 발로 움직이는 부분이 얼마나 있더라?” 같은 생각이 가장 먼저 들 것이다. 관련된 모든 부분을 수정해야 하니까!! 특히 핸들과 바퀴로 자동 주차가 될 수 있도록 계산하는 더 복잡한 로직을 추가하면서 기존 로직을 개선하고 서비스에 장애는 없어야 한다. 그럼 좀 힘든데요라는 말이 나오게 되고, 그러면 지금 돌아가는 거에 조금 수정 하면 되는 거 아니에요? 같은 소리를 듣게 된다. 근데 뭐 당연히 차 밑에 발이 달려있다고는 생각 안할 테지. 그게 정상이니까.

이런 개발 편의적 개발은 보통 일정 때문이라 생각한다. 누가 스마트하게 개발하고 싶지 않을까? 근데 다음달까지 자동 주차 되는 시스템을 만들라고 하면 나 같아도 그냥 걸어가는 자동차를 상상해서 만들 거 같다. 애초에 시간을 더 주든가…

어쨌든 다시 원래 내용으로 돌아가서, 다시 구체적인 클래스들로 보자면 아래와 같이 될 거 같다.

com.service.sub
   api
     sub
      -SubApi.java
    -Api.java
   oauth
    -Connect.java
    -OAuth2.java
    -ServiceProvider.java
   service
    -Service.java
  -main.java

api는 API 관련된 일들만 한다. 특히 Api.java

  1. API 호출 URL 생성
  2. API 호출 결과 파싱
  3. API들(SubApi.java들)을 관리

oauth.OAuth2.java는 인증 및 토큰 관련된 일만 하며

  1. OAuth2.java는 사용자 동의 받아 code 방식이든 client credential 방식이든 그 외의 방식이든 다양한 권한 부여 방식에 따라 URL을 만들고
  2. 권한 부여 방식에 따라 액세스 토큰을 요청하고
  3. 액세스 토큰을 갱신 한다

oauth.Connect.java는 애플리케이션에 대한 사용자 권한 부여로 연결되었음을 나타내는 액세스 토큰 등을 관리한다. 이 부분이 옛날에 가장 헷갈렸는데, 간단하게 보자면 Api들은 액세스 토큰이 필요한 반면, OAuth는 토큰의 발급만 신경 쓰지 어디에 누가 필요로 하는지는 신경 쓰지 않는다.

따라서 1. 애플리케이션과 사용자를 연결한다는 의미의 connect와 2. Api와 OAuth 통해 발급 받은 정보를 연결한다는 두 가지의 의미…가 아닐까? 싶다.

그래서 스프링 소셜의 AbstractOAuth2ServiceProvider를 상속하는 클래스 생성 시, 실제 OAuth 작업 수행하기 위해 OAuth2Template를 상속하는 클래스의 인스턴스를 받는다.

즉,

  1. ServiceProvider가 OAuth 인증 템플릿을 갖고
  2. OAuth 인증 템플릿은 액세스 토큰 및 토큰 갱신을 담당하고
  3. Connect는 인증된 정보들을 필드로 가지면서 API들로 전달한다

Connection은 ServiceProvider를 통해 간접적으로 OAuthTemplate을 갖고 $\to$ OAuthTemplate 통해서 액세스 토큰 발급과 갱신하고 $\to$ 그 연결 정보(ConnectionData)를 API 인스턴스로 전달하고 $\to$ API 인스턴스는 연결 정보를 사용해서 API를 호출한다.

개발에 마법은 없다. 단일 책임 원칙에 따라 각 클래스가 각자의 역할을 하지만, 각자 역할을 잘 연결해줘야 한다. 그리고 잘 연결하기 위해서는 잘 설계하고 잘 추상화 해야 한다. 이를 얼마나 스마트하게, 즉 1.견고한데 2.좋은 퍼포먼스를 내면서도 3.내부 로직을 유연하게 수정/개선할 수 있도록 설계 하는 것이 중요하며, 이를 위해 SOLID 원칙이 있는 게 아닌가 싶다.

Updated: