Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repository vs DAO #18

Merged
merged 2 commits into from
Mar 24, 2022
Merged

Repository vs DAO #18

merged 2 commits into from
Mar 24, 2022

Conversation

ghojeong
Copy link
Contributor

본래 구글 Big Query 를 해보겠다고 했지만,
회사 사내 스터디 및 도입이 완전히 망해버려서 주제를 바꾸었습니다.

최근에 회사에서 도입하느라 고심한 Repository Interface 디자인 패턴에 대해서 정리해보았습니다.
저도 아직 온전히 이해한게 아니라서, 틀린 말이나 표현이 있을 수 있습니다.

제가 틀린 부분이 많다고 생각하고 비판적으로 읽어주시면 감사하겠습니다.

@ghojeong
Copy link
Contributor Author

아, 마저 설명을 다 해내지 못했는데,
결국 하고 싶었던 것은 UserDTO, UserEntity 은 자료구조 (Data Structure) 로써 다루고,
User 도메인 모델은 비즈니스 로직을 가진 프레임워크에 의존적이지 않은 순수한 객체로써 다루고 싶다는 이야기였습니다.
제가 글재주가 없어서 제대로 정리를 해내지 못했네요.

@ghojeong ghojeong force-pushed the pyro branch 2 times, most recently from d149755 to 7616222 Compare March 13, 2022 00:31
Comment on lines +20 to +39
본격적으로 설명에 들어가기에 앞서,

"이름은 빈 칸일 수 없다." 라는 비즈니스 로직을 가진

User 엔티티를 하나 정의해두자.

```kt
@Entity(tableName = "user")
class User(
@PrimaryKey(autoGenerate = true)
val id: Long?,
val name: String
) {
init {
check(name.isNotBlank()) {
"이름은 빈 칸일 수 없습니다."
}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이름이 빈칸일 수 없다 같은 디비를 사용하지 않는 로직을 구현할때
뷰에서 자바스크립트로 체크하면 이런 로직을 빼도 괜찮을까요?
이런 예외?처리들을 어디에서 해줘야 가장 적합한지 고민이 많아서요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서버일 경우에는 ExceptionHandler 가 따로 있어서,
예외를 던지는게 권장됩니다. 최악의 경우라고 해보았자 500 Response 이니까요.

하지만 자바스크립트 혹은 모바일 기기에서 에러를 throw 하는 것은 거의 금기시 됩니다.
디폴트 ExceptionHandler 가 없어서, 핸들되지 에러일 경우 크래시가 납니다.
웹사이트의 경우는 그냥 새로 고침으로 땜빵이라도 할 수 있는데,
모바일 앱일 경우는 앱이 그냥 죽은거라서 사용자가 바로 삭제를 해버립니다.

따라서 에러를 던지는 로직은 가능한 서버에 있는 편이 좋습니다.

Copy link
Contributor

@kyupid kyupid Mar 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예를 들어서 회원가입 폼을 제출할때
이름이 빈칸이면 "제출"버튼을 비활성화시킨다던가
혹은 제출을 누르면 해당 빈칸을 체크하는 로직을 넣고 return false; 시키면 되지 않나해서요

Copy link
Contributor Author

@ghojeong ghojeong Mar 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다. 실제로도 그렇게 처리합니다.
하지만 해당 부분은 UI 로직으로, if/else 를 통해 처리하지,
절대로 throw 한 후에 catch 하려 하지는 않습니다.
이유는 혹시라도 catch 에 실패해서 크래시가 나면,
고객에게도 싸장님에게도 욕을 찰지게 먹기 때문입니다.

Copy link
Contributor

@kyupid kyupid Mar 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if/else 로 걸러진것 또한 throw 해야하나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇨 if/else 로 걸러진 것은 throw 하지 않습니다.
서버의 throw 로직을 프론트로 옮긴다면 if/else 문으로 옮겨야 한다는 점을 말하려고 한것이지,
프론트에서 예외처리를 하지 말라거나, 하는 의도는 아니었습니다.
즉 throw 만 아니면 프론트에서 예외처리를 해도 됩니다.

👍👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시라도 빈값이 들어온 것에 대한 처리를 해야하기 때문인가요?

세상에는 정말 별별 사람들이 많습니다.
가령 스페이스 3번을 누른 것을 닉네임으로 회원 가입을 하려 한다거나,
엔터 3번을 닉네임으로 회원가입을 하려는 사람들이 있습니다.

그리고 기존 회사 로직에서는, 이런 것에 대한 예외 처리를 안해두어서
스페이스 3번 닉네임과, 스페이스 7번 닉네임으로 회원 가입한,
전혀 다른 유저가 존재합니다.

이런 데이터를 프론트에게 건네주면, 프론트는 백엔드에서 이상한 데이터를 줬다고, 뭐라뭐라 불평을 하지요.
데이터 무결성에 대한 기준을 백엔드에서 세우지 못한것이니,
백엔드 책임이 맞기는 합니다... 속이 쓰려오지요 ㅋㅋㅋ

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아..그렇군요..
제가 생각한 상상범위 밖이었네요 그건..
예외처리는 앞단뒷단 다 철저하게 해주는게 좋겠네요.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null 이라고 표현하니까 직관적으로 이해가 안되신 것 같네요.
좀 더 실무적인 예로 들자면

업무 요건: 비밀번호는 8자리 이상이어야 한다. 최소 5곳의 외부 업체가 사용한다.

  1. 백엔드 개발자인 나는 openApi로 설계하여 개발하였다.

업무 요건 변경: 비밀번호는 10자리 이상이어야 한다.

  1. 업무 요건의 변경 사항을 공지했다.
  2. 백엔드 개발자인 나는 적절히 개발하여 배포했다.
  3. 하지만 이 공지를 받지 못한 업체에서는 수정 개발을 하지 못해 여전히 8자리 비밀번호로 api 호출을 하고 있다.

문제

  1. 불필요한 api 콜이 생긴다.
  2. 백엔드에서 방어로직을 하지 않았다라면 500 에러가 발생한다.
  3. 최악의 경우 요건을 충족하지 못하는 데이터가 저장되고 다음 흐름이 진행될 수도 있다.

하지만

  1. 이건 openApi인 경우이고, 단일 환경의 개발에서는 백엔드 개발자의 실수 or 프론트를 신뢰하고(?) 적당히 넘어가는 경우도 있습니다.
  2. 물론 이런건 QA에서 빠꾸먹고 고치는 것이 좋습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 상상할 수 있는 범위 내에서만 상황을 한정지으니까 이해를 못했던 거 같네요

단순히 빈칸 처리를 넘어서 open api 예시를 들어주시니까 더 확실히 이해가 가네요 감사합니다!

}
```

getValue 의 경우 key 에 해당하는 value 가 map에 없으면, <br>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

참고로 <br> 태그 대신에 띄어쓰기 3번하면 개행할수있어요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오! 이건 제가 전혀 모르던 꿀팁이네요. 감사합니다.

Comment on lines +182 to +183
한 줄만 작성하면 RemoteUserRepository 인스턴스를 의존성 주입을 받았을 때,
JPA 가 자동으로 구현해주는 save, count, findAll, findById 를 마음껏 사용할 수 있다.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Service
@RequriredArgsConstructor
public class FooService {
      private final RemoteUserRepsitory remoteUserRepository;

      public User getRemoteUser(Long id) {
           return remoteUserRepository.findById(id);
     }
}

이런식으로 DI 하면
remoteUserRepository.findById(id); 호출했을때
해당 인터페이스에 대해서 JPA가 자동으로 만든 구현체를 호출한다는 말씀이시죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다.
remoteUserRepository 를 개발자가 전혀 구현하지 않았는데,
(하다못해 마이바티스는 관련 쿼리라도 짜야하는데)
interface 만 extends 하면 알아서 프레임워크가 구현해서 의존성 주입까지 해주지요.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아시다시피 제가 마이바티스만 써서..ㅎㅎ
jpa 좋넹.,,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋ JPA 가 마이바티스를 누르고 인기가 있어지는 이유가 있기 하지요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오히려 Remote DB 가 아니라,
Local SQLite DB 에 접근하는 안드로이드 Room 이 큐에게는 더 익숙하시겠네요.
Room 이 마이바티스랑 많이 비슷하더라고요.

Comment on lines +189 to +193
그에 따라 프레임워크에 의존적인 인터페이스를 "DataSource" 라는 다른 명칭을 쓰기로 하였다.
그렇게 RemoteDataSource 와 LocalDataSource 를 표현해 보자면 다음과 같다.

DataSource 의 인터페이스와 Repository 의 인터페이스는 충분히 다를 수 있다고 생각했기 때문에,
DataSource 인터페이스는 Repository 인터페이스를 extends 하지 않는다.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataSource 인터페이스는 Repository 인터페이스를 extends 하지 않는다.

이거 RemoteDataSource 인터페이스는 UserRepository 인터페이스를 extends 하지않는다 라는 말씀인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다.
extends 하기를 포기한 결정적인 이유는 DataSource 의 인터페이스와,
Repository 의 인터페이스를 다르게 할 필요가 생겼기 때문입니다.

가령 DataSource 의 findById 는 UserEntity 자료구조를 반환하지만,
Repository 의 findById 는 User 도메인 객체를 반환합니다.

User 와 UserEntity 를 서로 다르게 분리할 필요가 생겼던 이유는,
레거시 때문에 기존 테이블의 자료구조가 프론트에서 필요한 자료구조와 다른 경우가 발생했기 때문입니다.

가령 UserEntity 에서 createdDate 는 String 값이지만,
User 도메인 클래스에서 createdDate 는 Calendar 입니다.

즉 프레임워크에 의존적인 인터페이스는,
개발자가 인터페이스를 직접 정의하는게 아니라 프레임워크의 가이드라인을 따르게 되는 경우가 많다.

### 비즈니스가 정말로 요구한 메커니즘
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 좀 어렵네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어렵게 생각할 거 없이,
제가 진짜로 필요했던 것은 DB 에 쿼리를 날리는게 아니라,

  1. DB가 되던 뭐가 되던 User 정보들을 가져오는 것과,
  2. 서버에 있는 데이터를 로컬과 동기화 하는것

이것 2가지가 필요했다는 점입니다.
그 밖의 count() 라던가 findBy 어쩌구는 사실 제게는 불필요했고, 딱히 알고 싶지 않았구요.

즉 개발자가 정의해야하는 인터페이스는,
프레임워크에서 요구하는 형태가 아니라,
개발자 본인이 정말로 필요로 하는 형태가 되어야 한다는 주장입니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개발자가 정의해야하는 인터페이스는,
프레임워크에서 요구하는 형태가 아니라,
개발자 본인이 정말로 필요로 하는 형태가 되어야 한다는 주장입니다.

👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개발자가 정의해야하는 인터페이스는,
프레임워크에서 요구하는 형태가 아니라,
개발자 본인이 정말로 필요로 하는 형태가 되어야 한다는 주장입니다.

근데 좀 의문인점은
프레임워크라는 것 자체가 프레임워크에 맞춰서 코드를 작성하도록 개발자에게 요구하는 거고
그 프레임워크를 쓴다는 것은 여러 편의성을 고려해서 개발자가 해당 프레임워크를 쓰는거라고 생각했는데요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프레임워크를 쓴다는 것은 여러 편의성을 고려해서 개발자가 해당 프레임워크를 쓰는거라고 생각했는데요

넵 맞습니다. "편의성" 을 고려해서 개발자가 프레임워크를 선택한거지,
프레임워크가 개발자를 선택한게 아닙니다.

즉 반대로 이야기하면, 프레임워크를 사용하다가 더 이상 편해지지 않는 순간이 오면
(즉 프레임워크에 맞추다 보니 편의성이 사라지는 순간이 오면)
프레임워크를 쓰지 않고 바닐라 언어로 개발자가 본인의 로직을 구현할 수 있어야 합니다.

하지만 대부분의 경우는 프레임워크를 사용하고 익히는데 급급해서,
사실 프레임워크 없이도 개발을 진행할 수 있었다는 사실을 팀이 망각하게 되는 경우가 많더라고요.

즉, 프레임워크가 편하지 않다면 그 프레임워크를 더 좋은 프레임워크로 교체하거나,
심지어 프레임워크를 사용하지 않는 선택지도 개발자가 고려할 수 있어야 한다고 생각합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 핵심 비즈니스 로직을 순수 바닐라 언어로 이루어진 도메인으로 구현하게 되면,
프레임워크가 바뀌거나 폐기되어도, 개발자 본인이 고심해서 작성한 로직은 재사용할 수 있게 됩니다.

반대로 로직자체가 지나치게 프레임워크에 의존적이면,
같은 프레임워크의 버전을 올리는 것조차 힘들어지는 순간들이 옵니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 그런 의미이셨군요
많이 배워갑니다 감사해요~!

Comment on lines +258 to +262
데이터베이스의 **엔티티** 는 relation(테이블)을 인스턴스화한 자료 구조이다.
비즈니스 로직을 가진 **도메인 모델** 과는 구분되어야 한다.

엔티티는 필연적으로 프레임워크에 의존적일 수 밖에 없지만,
도메인 모델은 바닐라 언어로 구현되어야 한다.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어렵군요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 코드상으로 설명을 다 못드렸는데,
위의 UserEntity 의 createdDate 가 DB 의 timestamp 형태의 String 인 것에 반하여,
User 도메인의 createdDate 는 자바에서 사용하기 편한 Calendar 객체임을 생각하면 될것 같습니다.

즉 UserEntity 는 DB 와 연관된 프레임워크 (마이바티스이던, JPA 이던)에서 사용하기 편한 구조이고,
User 도메인은 개발자가 바닐라 언어로 갖고 놀기 편한 구조가 됩니다.

제가 User 와 UserEntity 를 어떻게 독립적으로 구현했는지까지 설명했어야 했는데,
예시 코드를 만들다가 힘들어서 다 만들지 못했군요.
실무 코드에서는 이 의도대로 반영을 해두었습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이해했습니다 👍

Copy link
Contributor

@Hyune-c Hyune-c left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 요즘 많이 학습하고 있는 내용의 일부로 DDD와 육각형 아키텍처와 통하는 부분이 있네요.
제가 학습한 부분을 부연 설명하자면

  1. 기존의 JPA 학습 자료를 보면 JPA entity임에도 불구하고 entity라는 단어가 빠져있습니다. (ex. User)
  2. 하지만 최신 방법론에서는 도메인 영역과 영속성 영역을 분리하고 있습니다.
    • 예를 들면 도메인 영역의 객체를 User라고 하고, 영속성 영역을 Repository interface 로 추상화 시킨 후
    • 일반적으로 쓰이는 JPA Repository를 사용한다면, save(User user)의 형태로 매개변수를 도메인 객체로 전달하고 이를 구현하는 로직에서 UserJPAEntity로 생성하여 영속성을 구현합니다.
  3. 단점으로는 객체의 변환 비용이 발생합니다. (autoboxing - unboxing과 같은 문제)
    • 따라서 CQRS 패턴의 적용이 권장됩니다.

참고로 1, 2번은 책과 블로그에 널리 알려진 것이지만, 3번은 제가 추가한 내용입니다.


안드로이드를 기반으로 학습하시다보니 참고 서적이 다 생소하군요.
자바 백엔드 기반이라면 클린 아키텍처만들면서 배우는 클린 아키텍처를 참고하시면 될 것 같습니다.

Comment on lines +20 to +39
본격적으로 설명에 들어가기에 앞서,

"이름은 빈 칸일 수 없다." 라는 비즈니스 로직을 가진

User 엔티티를 하나 정의해두자.

```kt
@Entity(tableName = "user")
class User(
@PrimaryKey(autoGenerate = true)
val id: Long?,
val name: String
) {
init {
check(name.isNotBlank()) {
"이름은 빈 칸일 수 없습니다."
}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null 이라고 표현하니까 직관적으로 이해가 안되신 것 같네요.
좀 더 실무적인 예로 들자면

업무 요건: 비밀번호는 8자리 이상이어야 한다. 최소 5곳의 외부 업체가 사용한다.

  1. 백엔드 개발자인 나는 openApi로 설계하여 개발하였다.

업무 요건 변경: 비밀번호는 10자리 이상이어야 한다.

  1. 업무 요건의 변경 사항을 공지했다.
  2. 백엔드 개발자인 나는 적절히 개발하여 배포했다.
  3. 하지만 이 공지를 받지 못한 업체에서는 수정 개발을 하지 못해 여전히 8자리 비밀번호로 api 호출을 하고 있다.

문제

  1. 불필요한 api 콜이 생긴다.
  2. 백엔드에서 방어로직을 하지 않았다라면 500 에러가 발생한다.
  3. 최악의 경우 요건을 충족하지 못하는 데이터가 저장되고 다음 흐름이 진행될 수도 있다.

하지만

  1. 이건 openApi인 경우이고, 단일 환경의 개발에서는 백엔드 개발자의 실수 or 프론트를 신뢰하고(?) 적당히 넘어가는 경우도 있습니다.
  2. 물론 이런건 QA에서 빠꾸먹고 고치는 것이 좋습니다.

@ghojeong
Copy link
Contributor Author

ghojeong commented Mar 15, 2022

저도 요즘 많이 학습하고 있는 내용의 일부로 DDD와 육각형 아키텍처와 통하는 부분이 있네요.

피드백 감사합니다.
사실 이 구조는 마틴 파울러의 클린 아키텍처를, 재직중인 회사에 맞게 변형한 것 입니다.

이번에 엄청 구르면서 한가지 깨달은 점은,
에릭 에반스의 DDD 와
육각형 아키텍처에서 추구하는 바가 조금 다르다는 점입니다.

DDD 가 도메인 단위의 Vertical 한 설계 방법론을 제시한다면,
육각형 아키텍처는 Layer 단위의 Horizontal 한 설계 방법론을 제시한다는 느낌을 받았습니다.

그리고 특정 구조가 더 클린하거나 정답이 있기 보다는,
같이 협업하는 개발자들 간의 소통방식에 아키텍처를 맞춰야 한다는,
콘웨이의 법칙 을 경험했습니다.

가령 같이 모바일 개발을 진행하고 있는 iOS 팀 같은 경우에는,
서로의 지식수준이 비슷해서 기능 단위로 할일 을 쪼갰고,
그 기능이 곧 도메인 분류가 되어서 도메인 단위로 모듈 및 패키지 분리가 이루어지게 되었습니다.

반면 Android 팀 같은 경우에는 UI 를 잘 그리시는 분과,
비동기 통신 및 로컬 DB 를 잘 다루시는 분의 능력 차이가 명확해서,
Layer 단위로 모듈 및 패키지 분리가 이루어지게 되었습니다.

Layer 를 먼저 나누었다고, 도메인을 나누지 않는게 아니고,
도메인을 먼저 나누었다고, Layer를 나누지 않는게 아니지만,
개발 과정 및 최종 설계가 이렇게 달라질 수 있구나를 최근 많이 느끼게 되었네요.

@102092 102092 merged commit 6ecc0c6 into main Mar 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants