객체지향 설계 고수님들 조언 좀 부탁드립니다.

stylix의 이미지

몇달째 책이나 강의를 통해 설계관련 공부를 해도 도무지 해결이 안되는 문제가 있어 조언을 부탁드립니다.

많이들 하시는 OOP, Unit Test, SOLID 원칙, Design Pattern들 개념적으로는 잘 이해가 갑니다.
하지만 예제로 사용되는 코드들은 사실 너무 간단해서 막상 실무에 적용하긴 뭔가 한계가 있는 듯 합니다.
저를 제일 괴롭히는 문제는 Class들 상호간의 Relationship과 Testability입니다.

예를 들어...
처음에는 단순한 클래스(ClassA)를 작성합니다.
그러다가 클래스의 로직이 증가하면서 SRP에 따라 클래스(ClassB)를 분리합니다.
ClassB를 분리했기 때문에 원래 ClassA 자체의 로직이 좀 더 단순해지는 것까지는 좋습니다.
문제는 ClassA와 ClassB 사이의 (넓은 의미에서)의존성이 생긴다는 겁니다.
즉, ClassA와 ClassB는 유사한 책임을 가지고 있다가 분리된 것이어서 (예외도 있지만) 어떤식으로든 상호참조를 가져야 하는 경우가 많습니다.
더 나아가, 분리된 ClassA와 ClassB에 대해서도 점차 로직이 증가하면서 다른 클래스가 분리되면 그 클래스들과의 상호작용이 생기게 됩니다.
클래스 몇개만 만들어도 복잡한 연결관계를 피할 수 없더군요.

(또, 의존성을 줄이기 위해 상호참조를 일방참조로 바꾸는 과정에서 많은 경우 ClassA의 멤버변수 중 일부를 ClassB 에서도 멤버변수로 가지고 있어야 하는 경우도 있습니다.
이런 경우 여러 클래스가 같은 종류의 멤버를 가지고 있어서 코드 중복의 느낌이 납니다. 다만, 이건 복잡도의 문제는 아니기에 패스..)

클래스 로직을 단순하게 만드는 과정에서 역설적으로 클래스의 수가 증가하고
무엇보다도 클래스들 상호간의 참조관계가 폭발적으로 증가하게 됩니다.
운이 좋아서 디자인 패턴이라도 적용할 수 있는 형태면 그나마 좋지만 그건 일반적인 경우도 아니고요
또 하나의 문제는 이런 클래스는 여러 다른 객체들의 참조를 가지고 있기 때문에 테스트하기가 까다롭습니다.

클래스 하나를 단순하게 작성하기는 쉽습니다.
그런데 클래스를 단순하게 하면서 생기는 사이드 이펙트로서, 다른 클래스들 상호간의 관계가 복잡해 지는 문제를 도무지 어떻게 해야 할지 모르겠습니다.

여러 디자인 관련 책들을 봐도 SOLID 원칙등을 얘기하며 클래스를 단순하게 하고
클래스들 사이에 Loosely Coupling을 유지하기 위해 인터페이스 등를 활용하라라고만 나와 있지
정작 그런 과정에서 발생하는 클래스들간의 관계의 복잡성을 속시원히 해결해 주는 책은 없더군요.

이런 고민들은 비단 저 혼자만 하는 것은 아닐텐데... 혹시 조언 좀 해 주실 수 있는 분 계신가요?

P.S.
하스스톤 클라이언트 코드를 한번 디컴파일 해 봤는데 Card 라는 클래스는 클래스 하나에 멤버필드만 40여개이고, 라인수가 3000줄이 넘네요..
블리자드 개발자가 객체 지향 설계방법을 모르는 사람들도 아닐텐데, 이렇게 작성하는 것은 뭔가 한계가 있기 때문은 아닐런지..

익명 사용자의 이미지

수학, 과학, 공학의 공통점은 중복을 매우 싫어한다는 겁니다.
또 수학이나 과학과, 공학의 차이점은, 중복임을 알고 있음에도, 그게 더 싸게 먹히면 중복을 허용한다는 겁니다.
데이터베이스 이론의 정규화와 역정규화를 떠올려보면 좀 이해가 쉬우려나요?

설계의 원칙은 매우 간단합니다.
중복을 제거하는 겁니다.
제거하고 제거해서 알짜배기만 남기는게 설계입니다.
디자인패턴도 따지고 보면 중복을 제거하다보니 나오는 일종의 매크로 루틴 같은겁니다.

클래스끼리 의존성이 서로 꼬여있다면, 중복된 코드가 매우 많다는 의미가 됩니다.
중복이 의미의 중복일 수도 있고, 루틴의 중복일 수도 있고, 코드 자체의 모양이 중복일 수도 있습니다.
이런 중복을 제거하다보면 의존성을 단방향으로만 고정해 나갈 수 있습니다.
보통은 의미의 중복이 최우선이고, 그 다음 루틴과 코드 모양의 중복을 해결해야합니다.

물론 성능상의 문제나, 구현의 용이함을 위해 중복을 허용하거나, 의존성을 양방향으로 하는 경우도 있습니다.
양방향 의존성은 C++의 friend가 좋은 예죠. friend를 쓰는 클래스는 서로 같은 클래스라고 의미적으로 보고 짜야합니다.
즉, 클래스 전체를 구현하면, 코드가 방만하거나 읽기가 힘들기 때문에 다른 클래스로 분리를 했을 뿐이지,
같은 클래스 내에 있다고 해도 의미의 차이가 전혀 없거든요.
하지만, 중복허용이나 의존성 허용은, 설계 이후의 문제며, 설계시에는 무조건 단방향 의존성을 가지도록 만들어야 합니다.

익명 사용자의 이미지

그리고, 모든 경우에 적용되는 경우는 아니라는 걸 미리 밝힙니다만,

멤버 변수의 중복에 대해서 팁을 말씀드리자면,
두 클래스가 의미적으로 다름에도 불구하고, 여러개의 같은 멤버 변수를 필요로 한다면,
두 클래스 사이의 통신 프로토콜로 해석해야 하는 경우가 많습니다.
즉, 그 변수셋 자체를 클래스화 시켜야 하는 경우라는 거죠.

멤버 변수를 클래스화 시킴으로써, 두 클래스내에 있던 변수 코드의 중복이 제거되었고,
두 클래스는 프로토콜에 대해서 단방향 의존성을 가지게 됩니다.

익명 사용자의 이미지

> 즉, ClassA와 ClassB는 유사한 책임을 가지고 있다가 분리된 것이어서 (예외도 있지만) 어떤식으로든 상호참조를 가져야 하는 경우가 많습니다.

서로 의존성을 갖지않는 여러개의 작은 모듈로 나눈후, 그것들을 조합해서 사용하는식으로 사용하는것이 전형적인 접근방식입니다.
그 모듈은 클래스가 될수도있고, 함수가 될수도 있습니다. 기본적으로 가장 흔히 쓰이는 방법인, divide and conquer입니다.
A를 쪼개어서, B와 C(서브모듈)로 만들었는데 서브모듈들이 A에 대한 의존성을 갖고있다면, 설계를 잘못한경우가 대부분입니다.

다시말하자면, A는 B에대한 의존성을 갖을수있더라도, B가 A대한 의존성을 갖는다면(상호참조), 설계를 잘못한것은 아닌지에 대해 의심해봐야합니다.

Sdsf3qUr의 이미지

> 즉, ClassA와 ClassB는 유사한 책임을 가지고 있다가 분리된 것이어서 (예외도 있지만) 어떤식으로든 상호참조를 가져야 하는 경우가 많습니다.

저도 이 부분이 걸리네요. 여기서부터 다시 생각을 해보셔야 할 것 같습니다.

Haskell 같은 순수 함수형 언어를 살짝 맛보시는 것도 도움이 될 겁니다.

http://www.seas.upenn.edu/~cis194/lectures/01-intro.html

stylix의 이미지

우선, 답변들 감사합니다.

많은 분들이 상호참조를 문제 삼으셨네요.
하지만 상호참조에 대한 저의 의견은 이렇습니다.

먼저, 예로 든 ClassA와 ClassB 의 관계는 계층 관계가 불분명한 수평적 협력 관계를 가진 클래스를 가정한 것이었습니다.
ClassB가 ClassA의 논리적 서브 시스템이거나 서로 다른 레이어에 있는 로직은 아니라는 것이죠. (하지만, 이게 중요한 논점은 아닌 것 같고요)

일방참조가 상호참조보다 더 단순하다는 점은 저도 당연히 알고 있고요. 클래스를 추출하면서 이렇게 변환하기쉬운 경우는 저도 이렇게 코딩합니다.
하지만 상호참조가 있어서 설계를 의심해 봐야 한다거나, 설계 단계에서는 무조건 일방참조만 가져야 하다는 의견은 제 미천한 지식에도 좀 의아스럽습니다.

실제로 예전에 저도 이런 고민을 해 본적이 있고 검색도 해 봤었습니다.
http://stackoverflow.com/questions/613801/how-to-solve-cross-referencess-in-oop
같은 곳의 답변들만 봐도 상호참조 자체를 문제삼진 않았습니다.
제가 읽은 책 중에서도 상호참조를 피해야 할 Anti-Pattern으로 언급한 곳은 없었습니다.
마틴파울러의 책에서는 단방향 참조를 양방향 참조로 바꾸는 것을 엄연히 리팩토링의 한 기법으로 설명하고 있고요..
제 경험으로도 상호참조를 하면 펑션콜 한번에 해결되는 것을, 단방향 참조로 바꾸면 부가적인 클래스와 로직을 만들어야 하는 경우를 경험해 봤습니다.

상호참조가 일방참조보다 좋다는 것이 아닙니다.
상호참조의 사용여부가 설계 품질의 기준이 되는 것이 의아해서 그렇습니다.

과연 상호참조 때문에 설계가 잘못되었다고 생각하시나요?

익명 사용자의 이미지

데이터베이스에서 정규화를 안하고 역정규화가 더 좋다고 하는 말이나 다름 없습니다
정규화를 해봐야 어디를 역정규화할지 판단할 수 있죠

정규화 안하고 역정규화 하면 역정규화가 아닌 그냥 애드혹이고
단방향 의존이 바탕이 아닌 양방 의존은 설계가 아니라 그냥 멋대로의 구현일 뿐입니다
그런 사고방식으로는 영원히 좋은 설계라는 걸 하기 힘들겁니다

stylix의 이미지

죄송하지만 좀 더 설명을 부탁드려도 될까요?

그렇다면 개개의 클래스 레벨에서 모든 클래스들이 단방향으로 참조를 가져야 한다는 의미인가요?

그렇다면 흔히 경험하게 되는.. 예를 들어

1. ClassB가 비동기적으로 연산을 한 후 결과를 다시 ClassA에게 돌려줘야 하는 경우에는?

2. ClassA->ClassB, ClassB->ClassC, ClassC->ClassA 와 같은 순환참조는 어떻게 보시는지?

3. 혹시, 이와 관련된 내용의 책이나 자료가 있다면?
(데이터베이스의 경우는 모든 책에서 정규화, 역정규화 내용이 나와서 이해하기 쉬운데,
앞서 언급했듯이 객체지향 설계 책에는 이런 내용들이 잘 안나오던군요)

익명 사용자의 이미지

1번은 결코 양방향이 아닙니다
호출하고 결과를 받는 것은 단방향입니다
동기든 비동기든 하는 건 구현의 문제고 설계단계에서 언급할 필요조차 없습니다

2번은 그와같이 순환하는 재귀호출 함수를 생각해보면 됩니다
그런 함수를 누가 짜놨는데 고치려고 하면 욕부터 하시겠죠
잘 설계된 함수와 클래스는 의미적으로 다르지 않습니다 차이라면 데이터 추상인지 프로시저 추상인지 차이뿐입니다
순환참조를 설계했다면 설계를 한 쪽보다 안한쪽에 더 가깝습니다

익명 사용자의 이미지

3번은 첫댓글에서 밝혔듯이
수학 과학 공학에서 늘 가르쳐주는게 설계입니다
중복만 다 제거해도 설계는 더 이상 할게없어요

stylix의 이미지

아.. 첫 댓글 달아주셨던 분이시군요..

저에게는 뭔가 고수준의 말씀을 하시는듯 합니다..
저 같은 초보에게도 중복이란 데이터의 중복이나 로직의 중복으로 당연히 제거해야 할 대상으로 별 의심없이 받아드리고 있는데요..

"중복을 제거하는 과정에서 의존성을 단방향으로 고정할 수 있다" 는 말이 아직 어렵네요.
"의미의 중복"이라는 말도 그렇고요.
의미의 중복을 먼저 해결하고 루틴의 중복을 해결한다고 하셨는데..
의미란 말이 너무 포괄적인 단어라..
무엇보다 그 중복을 제거하는 것과 의존성의 방향성을 정하는 것이 어떻게 관련이 있는지...

가르침 부탁드립니다.

익명 사용자의 이미지

휴대폰으로 쓰는거라 긴내용은 힘들고
두 클래스에서 중복되는 의미를 합치면 거진 둘 중 하나가 됩니다
두 클래스 모두가 참조하는 다른 클래스가 만들어지거나, 둘 중 하나가 다른 하나를 참조하게 되거나
이런식으로 단방향 의존성이 만들어집니다

의미 중복은 입력값에 대한 결과값이 같은 거라고 보면 이해가 쉬울 겁니다
a.push(b)나 b.pull(a)나 동작은 다르지만 입력값 a b에 대해서 동일한 결과를 가져옵니다

보통 클래스마다 넣는 equal이나 tostring같은 것도 서로 다른 위치에 있을뿐 같은 일을 하죠
그래서 이런 메소드만 있는 부모클래스 object를 만들어서 의존성을 단방향으로 만듭니다

설계는 복잡한 것을 단순하게 바라보려고 하는 것이고
중복제거도 복잡한 것을 단순하게 만듭니다
의미적으로 동일하죠

stylix의 이미지

아...무릎을 탁 치게 하는 뭔가가 있네요.
중복제거를 의존 방향성의 관점에서 바라본다는게 신선한 충격입니다.
정말 도움이 많이 되었습니다.
감사합니다.

조금 더 궁금한 것들이 있습니다.
제가 위에 링크 건 StackOverflow의 답변들 중에 그냥 지나쳐 버리기 어려운 게 있습니다.

(참고로 질문자는 두 Person 객체가 Spouse 라는 속성으로 상호참조를 가지는 것이 잘못된 설계인지 물었습니다.)
"The cross-referencing issue you describe isn't an artifact of any design process but a real-life characteristic of the things you're representing as objects, so I don't see there's a problem."
즉, 설계의 문제가 아니라 실 세계 특성이 그러하기 때문에 문제가 안 될 거라는 답변인데요.

그 답변처럼 현실 세계에서는 "원시적으로" 상호참조나 순환참조를 가지는 객체나 개념들이 존재합니다.
그와 관련된 소프트웨어를 설계할 때도 변형을 가하여 상호참조나 순환참조를 제거해야 한다고 보시나요?
견해가 궁금합니다.

익명 사용자의 이미지

이야기했지만 저는 양방향 의존을 아예 쓰지 말라고 한게 아니라 설계시에 쓰지 말라고 했습니다
저도 구현할때 하나의 오브젝트를 쪼개서 양방향 의존이 되도록 많이 씁니다
설계 개념상에서는 간단하지만 실제구현은 그처럼 우아하지 않은 경우는 꽤나 많죠
하지만 저는 애초에 하나인 것을 쪼갤뿐이라 의미적으로 단방향 의존 설계와 개념이 달라지지 않도록 합니다

그리고 3중 순환참조는 고려해본적 조차 없습니다

익명 사용자의 이미지

person과 spouse의 양방향 의존이 반드시 필요하다면
설계시에는 둘을 뭉뜽그려서 couple로 놓습니다
외부와의 인터페이스는 couple만이 담당하고
내부적으로 person과 spouse가 돌면 아무 문제가 없죠

stylix의 이미지

답변 감사합니다.
말씀하신 의도는 잘 이해했습니다.

혹시, 괜찮으시다면 저에게 메일 한 통만 보내 주실 수 있으신가요?
개인적으로 드릴 말씀이 있는데 익명으로 댓글을 다셔서 쪽지를 보낼 수가 없네요..
많이 귀찮게 하진 않겠습니다.

제 주소는
----------------
입니다.

----------------
답변 주셔서 제 메일 주소는 삭제합니다.

HDNua의 이미지

답변은 아니고, 북마크합니다. 시간 날 때 읽어보면 좋겠네요.

저는 이렇게 생각했습니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.