Search
🤖

객체지향 주요 개념

1. OOP 목적

코드의 가독성과 재사용성을 높여서 유지보수를 쉽게 하는 것

2. OOP 핵심 개념

"객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것" -조영호

3. 인터페이스와 추상 클래스의 활용

가. 추상체와 재사용성

특정 코드의 대한 재사용성을 높이기 위해서 사용방법(시그니처)에 대해 정립
해당 코드의 생산자와 사용자는 사용방법을 따르기로 계약
본 계약은 생산자가 만든 함수의 시그니처를 사용자에게 따를 것을 강제함
→ 사용자 임의로 특정 코드의 틀을 바꾸면 재사용성이 떨어짐
인터페이스와 추상 클래스의 추상메소드를 구현하지 않으면 컴파일 불가
→ 인터페이스와 추상 클래스의 추상메소드가 재사용성을 위한 일종의 계약

나. 추상 클래스 vs 인터페이스

사용 방법
→ 추상 클래스: 엄격한 is-a 관계(부모-자식 간 상속 관계)에 사용됨
개와 고양이는 모두 포유류 클래스를 상속 받는 관계며, 추상 메소드를 각각 구현하고 추상 클래스에 구현된 generateHeat을 상속 받아서 사용함
public abstract class Mammal { public void generateHeat() { System.out.println("Generate Heat"}; public abstract void makeNoise(); } public class Dog extends Mammal { public void makeNoise() {System.out.println("Bark"}; } public class Cat extends Mammal { public void makeNoise() {System.out.println("Naaa"}; }
Java
복사
→ 인터페이스: 엄격하지 않은 관계에서 사용됨
개와 도마뱀은 각각 포유류와 파충류의 추상클래스를 상속 받는 관계이나, 모두 이름을 필요로 하므로 상속관계와 관계 없어 interface의 추상 메소드를 받아서 구현함
public interface Nameable { public String getName(); public void setName(String aName); } public class Dog extends Mammal implements Nameable { String dogName; public String getName() { return dogName; } public void setName(String myName) { dogName = myName}; } public class lizard extends Reptile implements Nameable { String lizardName; public String getName() { return lizardName; } public void setName(String myName) { lizardName= myName}; }
Java
복사
기능적 차이
→ 추상 클래스: 추상 메소드 한 개 이상 포함 + 구상 메소드 포함 가능
→ 인터페이스: 오직 추상 메소드만 포함
다중 상속
→ 추상 클래스: 다중 상속 지원 X
→ 인터페이스: 다중 상속 지원 O

4. 책임-주도 설계 절차

1. 유스케이스 작성 2. 도메인 모델 설계 3. 책임-주도 설계로 통합

가. 기능: 유스케이스 작성

유스케이스에서 최초의 책임(시스템 책임, 가장 거대한 책임)을 이끌어 낼 수 있음
유스케이스(Use Case): 사용자가 시스템을 활용하는 과정 전반에 대한 것을 텍스트로 정리한 것
ex) 유스케이스 요약: 예금주(사용자)는 중도 해지 이자액을 알고 싶다
주요 시나리오
1.
예금주가 정기예금 계좌를 선택한다
2.
시스템은 정기예금 계좌 정보를 보여준다
3.
예금주가 금일 기준으로 예금을 해지할 경우 지급받을 수 있는 이자 계산을 요청한다
4.
시스템은 중도 해지 시 지급받을 수 있는 이자를 계산한 후 결과를 사용자에게 제공한다

나. 구조: 도메인 모델 설계

도메인 모델을 통해 변경 가능성이 적은(근본적인) 구조를 찾아낼 수 있음
도메인 모델: 관심을 갖는 특정 영역에 대해 추상적이지만 간단한 구조를 만드는 것
→ 도메인: 관심을 갖는 특정 영역에 대해 사용자가 가지고 있는 이미지
→ 모델: 특정 지식에 대해 강조하고 축소하여, 추상적이지만 간단한 구조를 만드는 것
ex) 정기예금(기간, 해지여부) - 계좌(계좌번호, 예금액) - 이자율(금리) - 이자(금액, 지금일자)
도메인 모델 설계 시 주의할점: 도메인 설계 시, 사용자의 입장에서 구조를 잡을 것
→ 도메인 모델 작성 시, 설계자가 그리는 모델과 실제 사용자가 그리는 모델 간에 차이가 발생할 수 있음
→ 새로운 서비스를 설계할 때, 기존에 존재하지 않았던 서비스라면 사용자가 해당 서비스에 대해 어떤 이미지를 가지고 있는 지 알 수 없기 때문임

다. 통합: 책임-주도 설계

책임-주도 설계 목적: 기능의 불안정성을 안정적인 구조에 통합하는 과정을 구체화함
→ 기능의 불안정성과 직관성을 구조의 안정성과 추상성을 구체적인 절차에 따라 통합함
설계 과정
1) 시스템 기능을 파악: 유스케이스를 통해 시스템 책임(최초의 책임)을 파악함
ex) 예금주가 금일 기준으로 예금을 해지할 경우 지급받을 수 있는 이자 계산을 요청한다
2) 시스템 기능을 객체의 책임으로 변환
강조) 객체 보다 중요한 것은 메시지임. 메시지를 구체화하고 해당 메시지에 적합한 객체를 선택할 것
→ 해당 메세지의 주어 가능 여부에 따라 메세지의 수신 객체가 될 수 있음
→ 메세지의 수신 객체는 추상화가 가능한(여러 객체가 대신할 수 있는) '역할'이 됨
→ 객체를 선택할 때, 도메인 모델의 구조에서 해당되는 개념을 차용함
3) 객체의 책임을 더 작은 책임으로 분할
ex) '정기예금' 객체는 해지 일자를 전달받아서 이자 계산을 시작하는 책임을 갖는다
'정기예금' 객체는 해당 일자가 약정 기간에 포함되는 지 확인하는 책임을 갖는다
'정기예금' 객체는 해당 일자가 약정 기간에 포함되면 '이자' 객체에게 이자 계산을 요청한다
~~
객체지향의 사실과 오해, p200 참고
4) 분할된 책임을 수행할 객체를 생성하거나 찾아서 해당 책임을 할당함
→ 마찬가지로 분할된 책임을 맡을 객체를 생성하거나 찾기 전에 해당 책임이 담긴 메세지를 구체화하고 이 메세지에 어울리는 객체를 탐색할 것
5) 4번의 객체가 해당 책임을 수행하는데 도움이 필요한 경우, 협력할 역할 또는 객체를 찾음
6) 해당 역할 또는 객체에게 협력에 대한 책임을 할당함으로써 협력 관계를 만듬
책임과 협력 관계가 모두 파악이 되면 클래스 변환함
대부분 구현단계에서 설계한 부분이 수정되므로 설계는 큰 틀에서 빠르게 작성하고 구현 단계를 거치면서 수정할 것

5. SOLID 원칙

가. SOLID 원칙 도입 배경

현실세계의 관계를 그대로 코드로 옮길 경우, 설계적 문제 발생
설계적 문제란 재사용성이 매우 떨어지는 코드가 생산되는 것
소프트웨어 설계의 이해를 돕고, 재사용성을 높이며 유지보수를 편리하게 하기 위해 SOLID 원칙 제시
재사용성이 떨어지는 코드는 다음의 특징이 있음
→ 경직성: 프로그램의 일부분에 대한 수정에 따라 다른 부분도 수정해야 함
→ 취약성: 오류의 원인이 뜬금없는 곳에서 있음
→ 부동성: 이식이 불가능함(or 다른 부분에 재사용이 불가능함)

나. SRP(Single Responsibility Principle)

단일책임 원칙 목표: 하나의 메소드에 대한 변경이 다른 메소드에 영향을 주지 않는 것
→ 불필요하게 컴파일 하지 않는 것
하나의 클래스는 하나의 기능(or 책임)을 수행함
→ 특정 클래스에 대한 주석을 명시할 때, '그리고'라는 설명이 추가되면 SRP가 위반되는 경우로 해석 가능
ICalculator 인터페이스에는 직선과 삼각형 그리고 사각형에 대한 계산 등 세 가지의 기능이 포함되어 있기 때문에 SRP의 원칙에 위배됨
public interface ICalculator { double calLine(Coordinate first, Coordinate second); double calTriangle(Coordinate first, Coordinate second, Coordinate third); double calSquare(Coordinate first, Coordinate second, Coordinate third, Coordinate forth); } // to // 바꿔보자
Java
복사

다. OCP(Open/Close Principle)

개방/폐쇄 원칙 목표: 기존 클래스에 대한 변경 없이 코드를 확장하는 것
→ 레거시 코드에 대한 걱정 없이 시스템 확장 가능
부모 클래스에 대한 변경 없이 자식 클래스를 통해서만 코드를 확장함
→ 부모 클래스에 대한 변경이 더이상 없기 때문에 Closed
→ 요구사항에 따라 자식 클래스는 계속 확장하기 때문에 Open
이하 예제에서 Rectangle을 부모클래스로 설계하면 Circle의 calArea를 구할 때 부모 클래스를 변경해야 함
→ Circle의 넓이를 구할 때, 지름이라는 추가적인 변수가 필요함
→ Shape이라는 추상 클래스를 만들고 추상 메소드로 getArea()를 선언하면 이하 자식 클래스 Rectangle과 Circle을 생성하면 다른 부모 코드에 대한 수정을 가하지 않아도 됨
class Rectangle{} class CalculateArea{ // 생략 public double calcArea(Rectangle r){ } }
Java
복사

라. LSP(Liskov Substitution Principle)

리스코프 대체 원칙 목표: 부모 클래스의 필드와 메소드는 모든 자식 클래스에서 편법 없이 그대로 사용해야 함
→ 설계에 일관성이 결여되면 의도하지 않은 유지보수 문제로 이어질 수 있음
이하 예제를 보면, Square의 생성자는 부모 클래스인 Rectangle 생성자와 달리 매개변수를 1개만 전달 받음. super() 사용하여 기능면에서는 문제가 없지만 설계의 일관성이 결여되었기에 추후에 문제 발생 여지가 있음
abstract class Shape { } class Rectangle extends Shape { public Rectangle(double l, double w){ } } class Square extends Rectangle { public Square(double s) { super(s, s); } }
Java
복사

마. ISP(Interface Segregation Principle)

인터페이스 분리 원칙 목표: 큰 단위의 인터페이스 보다 작은 단위의 인터페이스를 다수 생성
→ 여러 인터페이스를 합성하여 객체를 설계하는 것이 추후 예상치 못한 확장에 대처할 때 유리
Interface를 아래와 같이 작은 단위로 쪼개면 추후 시스템 확장 시 요구사항을 정확하게 맞출 수 있음
interface IEat { public void eat(); } interface IAbsorbElectronic { public void absorbElectronic(); } interface IMakeNoise { public void makeNoise(); } class Dog implements IEat, IMakeNoise { } class Keyboard implements IAbsorbElectronic, IMakeNoise { }
Java
복사

바. DIP(Dependency Inversion Principle)

의존성 역전 원칙 목표: 추상화에 의존하여 코드가 결합되어야 함
→ 추상화에 의존한 설계가 된다면 객체의 구상화는 컴파일 타임이 아니라 런 타임에 이루어짐
의존성 역전을 위해 객체에서 행위 부분을 다른 객체로 분리함
→ 해당 행위 부분은 상속관계가 아닌 객체에서도 사용 가능(재사용성 Up)
public class DIP { public static void main(String[] args){ Mammal cat = new Mammal(new CatNoise()); Mammal dog = new Mammal(new DogNoise()); System.out.println(cat.makeNoise()); System.out.println(dog.makeNoise()); } } interface MakingNoise { abstract String makeNoise(); } class CatNoise implements MakingNoise { @Override public String makeNoise() { return "Na"; } } class DogNoise implements MakingNoise { @Override public String makeNoise() { return "Bark"; } } class Mammal { MakingNoise speaker; public Mammal(MakingNoise sp){ this.speaker = sp; } public String makeNoise(){ return this.speaker.makeNoise(); } }
Java
복사

Reference

Matt Weisfeld, The Object-Oriented Thought Process
조영호, 객체지향의 사실과 오해