프로그래밍을 하다 보면 `절차지향`과 `객체지향`이라는 말을 자주 듣게 됩니다. 저도 코딩을 하면서 이런 용어에 큰 의미를 두지 않았습니다. "작동만 하면 되지 않나?"라는 단순한 생각으로 코드를 짜고 끝냈습니다. 하지만 스터디를 시작한 이후로 제 생각이 완전히 틀렸다는 걸 깨닫게 되었습니다.
사실, 이때까지 제가 짠 코드를 다시 보는 일이 별로 없었습니다. 하지만 스터디를 통해 제 작성한 코드 피드백을 받고 제 코드를 돌아보니, 제가 작성한 코드가 대부분 절차지향적이라는 피드백을 받았습니다. 그 순간부터 많은 고민이 시작됐습니다. "자바는 객체지향 언어라고 하던데, 나는 왜 절차지향적으로 코드를 짜고 있는걸까?"라는 질문이 머릿속을 떠나지 않았습니다. 사실 그동안 "객체지향"이라는 단어를 종종 봤고, 자바가 객체지향 언어라는 것도 알고 있었지만, 정작 코드를 쓸 때는 그 개념을 전혀 고려하지 않았었습니다.
이런 고민이 깊어지면서 몇 가지 의문이 생겼습니다.
- 객체지향이 대체 뭐지?
- 왜 객체지향을 사용하는 걸까?
- 어떻게 코드를 짜야 더 효율적이고 좋은 코드가 될까?
이 질문들에 대한 답을 찾고 싶어서 자료를 뒤지던 중, `오브젝트`라는 책을 발견했습니다. 객체지향의 개념과 실무에서의 활용을 깊이 다룬 책이라는 설명에 끌렸고, 마침 인프런에서 이 책을 기반으로 한 강의도 있다는 걸 알게 됐습니다. 망설임 없이 강의를 구매했고, 하나씩 들으며 제 코딩 스타일을 돌아보게 됐습니다.
이 포스팅은 해당 강의를 바탕으로 제가 느낀 점과 깨달음을 정리한 기록입니다. 해당 과정은 단순히 `작동하는 코드`를 넘어, `좋은 코드`를 쓰기 위한 저의 첫걸음라고 생각합니다. 저처럼 객체지향에 대해 막연한 궁금증을 가진 분들께 조금이나마 도움이 되기를 바랍니다.
절차지향 프로그래밍 VS 객체지향 프로그래밍
절차지향 프로그래밍(Procedural Programming)
프로그램을 단계별 절차나 순서에 따라 작성하는 방식. 주로 함수를 중심으로 코드를 구성하며, 데이터를 처리하는 로직을 함수 안에 넣음
특징 | 코드가 위에서 아래로 순차적으로 실행 데이터를 변수에 저장하고, 함수로 해당 데이터를 처리 |
장점 | 단순하고 직관적이라 작은 프로그램 작성에 적합 실행 속도가 빠름 |
단점 | 코드가 길어지면 관리하기 어려움 데이터와 함수가 분리되어 있어서 유지보수가 힘들 수 있음 |
프로그램을 객체라는 단위로 나누고, 객체들이 서로 상호작용하도록 설계하는 방식. 객체는 데이터와 메서드를 가짐
특징 |
캡슐화 : 데이터와 기능을 하나로 묶고, 외부 접근을 제어
상속 : 기존 객체의 특성을 물려받아 새 객체를 만듦
다형성 : 같은 기능이 상황에 따라 다르게 동작
추상화 : 복잡한 세부사항을 숨기고 핵심만 보여줌
|
장점 |
코드 재사용성이 높고, 유지보수가 쉬움
대규모 프로젝트에 적합
|
단점 |
설계가 복잡할 수 있고, 초보자들에게는 학습이 어려울 수 있다.
실행 속도가 절차지향보다 느릴 수 있음.
|
절차지향적인 코드의 문제점
절차지향적인 코드의 문제점은 주로 코드의 규모가 커지거나 복잡성이 증가할 때 두드러집니다.
1. 코드 중복과 재사용성 부족
- 절차지향에서는 비슷한 작업을 할 때마다 함수를 새로 작성하거나 기존 코드를 복사해서 붙이는 경우가 많습니다.
2. 데이터와 로직의 분리
- 절차지향적인 코드에서 데이터는 그냥 변수나 배열 같은 형태로 존재하고 로직은 그 데이터를 처리하기 위해 따로 만든 함수에 들어가 있습니다. 해당 방식은 코드가 길어질수록 어떤 함수가 어떤 데이터를 다루는지 파악하기 힘들어집니다.
3.유지보수 어려움
- 코드가 순차적으로 짜여 있어서 한 부분을 수정하면 다른 부분에도 영향을 줄 가능성이 큽니다 (의존성 문제).
- 작은 변경에도 전체 코드를 점검해야 하니 시간이 오래 걸립니다
객체지향적으로 코드를 어떻게 구성할까 ?
1. 데이터의 getter를 사용해서 판단하고 결정하는 로직을 그 데이터로 옮겨라
`Tell, Don't ask.` 객체에게 데이터를 요구(Ask) 하지 말고, 객체에게 일을 시켜라(Tell) 입니다.
쉽게말하면, 데이터를 꺼내서 밖에서 뭔가를 판단하거나 결정하지 말고, 그 데이터가 있는 객체 스스로 로직을 처리하게 하라는 뜻입니다.
데이터를 사용하는 로직을 데이터를 보유한 클래스 쪽으로 옮기면 데이터가 수정될 때 변경의 영향 범위를 데이터 클래스 내부를 제한할 수 있게 됩니다. -> 클래스 수정 용이
이렇게 데이터를 사용하는 로직을 데이터를 보유한 클래스로 이동시키는 작업을 객체지향 용어로 표현하면 `책임의 이동`(Shift or Responsibility) 라고 합니다.
Getter로 판단하는 코드
class Student {
private int score;
public Student(int score) {
this.score = score;
}
// Getter
public int getScore() {
return score;
}
}
class Main {
public static void main(String[] args) {
Student student = new Student(85);
// Getter로 데이터를 꺼내서 외부에서 판단
int score = student.getScore();
if (score >= 60) {
System.out.println("합격!");
} else {
System.out.println("불합격!");
}
}
}
- getScore()로 점수를 꺼내서 Main 클래스에서 합격/불합격을 판단하고 있음.
- 문제: 점수와 관련된 로직(합격 판단)이 Student 객체 밖에 흩어져 있음.
[리팩토링] 로직을 데이터로 옮기기
class Student {
private int score;
public Student(int score) {
this.score = score;
}
// 로직을 Student 클래스 내부로 옮김
public boolean isPassed() {
return score >= 60;
}
}
class Main {
public static void main(String[] args) {
Student student = new Student(85);
// 객체에게 직접 물어봄
if (student.isPassed()) {
System.out.println("합격!");
} else {
System.out.println("불합격!");
}
}
}
- 이제 getScore()로 점수를 꺼내지 않고, Student 객체가 스스로 합격 여부를 판단.
- isPassed()라는 메서드가 데이터를 기반으로 로직을 처리합니다.
2. 의존성 개수 줄이기
class ScoreCalculator {
public boolean isPassed(int score) {
return score >= 60;
}
}
class Printer {
public void printResult(String name, boolean passed) {
System.out.println(name + "님은 " + (passed ? "합격" : "불합격") + "입니다.");
}
}
class Main {
public static void main(String[] args) {
int score = 85;
String name = "홍길동";
ScoreCalculator calculator = new ScoreCalculator();
Printer printer = new Printer();
boolean passed = calculator.isPassed(score);
printer.printResult(name, passed);
}
}
Main이 ScoreCalculator와 Printer에 직접 의존(의존성 2개).
-
- 학생 데이터(score, name)가 흩어져 있고, 도메인(학생이라는 개념)이 코드에 반영 안 됨.
- 합격 로직과 출력 로직이 학생과 무관한 별도 클래스에 분리되어 의존성이 더 생김.
[리팩토링] 도메인 구조에 맞춰 리팩토링
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
// 합격 여부 판단 로직을 Student 내부로
public boolean isPassed() {
return score >= 60;
}
// 출력 로직도 Student가 처리
public void printResult() {
System.out.println(name + "님은 " + (isPassed() ? "합격" : "불합격") + "입니다.");
}
}
class Main {
public static void main(String[] args) {
Student student = new Student("홍길동", 85);
student.printResult();
}
}
- Student라는 객체가 도메인(학생)을 반영해서 이름과 점수를 묶음.
- 합격 판단(isPassed)과 출력(printResult) 로직을 Student 내부로 옮김.
- Main은 이제 Student 하나에만 의존(의존성 2개 → 1개로 감소)
객체지향 설계 원칙
- 객체를 선택하기 전에 요청을 결정하기 떄문에 코드를 수정하지 않고도 협력하는 객체를 교체할 수 있게 해줍니다.
객체의 `행동을 먼저 구현`하고 행동에 필요한 데이터를 나중에 선택하라
- 협력하는 객체가 데이터에 의존하지 않도록 만들어주기 때문에 코드를 수정하지 않고도 데이터를 변경할 수 있게 해줍니다.
즉, 어떤 객체가 필요하고 그 객체가 어떤 데이터를 저장해야 하는지 보다 `행동을 먼저 결정`하고 `객체와 데이터는 나중에 결정`해야 한다는 것입니다.
객체지향 설계 흐름
협력을 위한 문맥 결정 -> 필요한 책임을 식별 -> 책임을 수행할 객체를 선택 -> 책임 구현 -> 데이터 결정
객체가 외부에 제공해야 하는 책임
- 객체를 생성하거나 계산을 하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
- private로 캡슐화된 상태(데이터)에 관해 아는 것
- 관련된 객체에 관하여 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관하여 아는 것
아는 것과 관련된 책임을 내부에 핻아하는 데이터를 저장해야 한다는 것 x
책임은 행동 관점이기 때문에 어떤 것을 아는 책임을 할당받았다는 것은 해당 데이터를 저장해야 한다는 것이 아니라 정보에 대해 대답할 . 수있어야 한다는 것을 의미합니다.
책임이 중요한 이유?
세부사항과 관련된 결정을 뒤로 미루고 `객체들의 협력 구조에 초점`
책임 주도 설계
- 애플리케이션이 제공할 기능 파악
- 애플리케이션의 기능 요구사항을 시스템의 책임으로 변환
- 시스템의 책임을 객체의 책임으로 변환
- 책임을 담당할 적절한 객체 선택
- 객체의 책임 일부를 수행하기 위해 외부의 도움이 필요하다면 다른 객체에게 도움을 요청
- 이 요청을 또 다른 객체의 책임으로 변환
- 책임을 담당할 적절한 객체 선택
GRASP
GRASP 패턴을 따르면 코드가 더 깔끔해지고, 유지보수가 쉬우며, 객체지향 원칙(단일 책임 원칙, 낮은 결합도 등)에 부합하게 됩니다.
GRASP의 주요 패턴
- Information Expert (정보 전문가)
- Creator (생성자)
- Low Coupling (낮은 결합도)
- High Cohesion (높은 응집도)
- Polymorphism (다형성)
- Protected Variations (변경 보호)
- Indirection (간접화)
- Pure Fabrication (순수한 가공물)
- Controller (컨트롤러)
`information Expert (정보 전문가)`
문제 : 책임을 객체에게 할당하는 일반적인 원칙은 무엇인가?
해결방법 : 책임을 수행하는데 필요한 정보를 가장 많이 알고 있는 객체에게 할당하라
어떤 상태를 수정하거나 질문에 답할 수 있는 객체에게 책임 할당
객체지향 관점에서는 객체가 내부에 해당 데이터를 저장하고 있지 않더라도 정보에 대한 질문에 답할 수 있다면 해당 정보를 책임지고 있는것으로 간주합니다.
ex) 학생은 몇 살인가요 ?
객체가 내부에 나이라는 데이터를 저장하고 있는지는 중요하지 않습니다. 나이에 대한 질문에 답변만 할 수 있으면 됩니다
`Creator (생성자, 창조자)`
문제 : 새로운 인스턴스를 생성하는 책임을 어떤 객체에게 할당할 것인가?
해결방법 : 다음 중 한 가지라도 만족할 경우 A의 인스턴스를 생성할 책임을 B에게 할당하라
- B가 A를 포함하거나 참조한다 `Low Coupling (낮은 결합도)`
- B가 A를 기록한다 `Low Coupling (낮은 결합도)`
- B가 A를 긴밀하게 사용한다 `Low Coupling (낮은 결합도)`
- B가 A를 초기화하는 데 필요한 정보를 알고 있다 `Information Expert (정보 전문가)`
`Low Coupling (낮은 결합도)`
문제 : 어떻게 낮은 의존성을 유지하고, 변경에 따른 영향을 줄이면서, 재사용성을 높일 수 있을까?
해결방법 : 설계의 전체적인 결합도를 낮게 유지할 수 있도록 책임을 할당하라
`High Cohesion (높은 응집도)`
문제 : 어떻게 낮은 결합도를 유지하고, 변경에 따른 영향을 줄이면서, 재사용성을 높일 수 있을까?
해결방법 : 높은 응집도를 유지하도록 책임을 할당하라
`Polymorphism (다형성)`
문제 : 타입을 기반(ex. 금액 할인 정책 타입과 비율 할인 정책 타입)으로 유사하지만 서로 다르게 행동(ex. 할인 요금을 타입에 따라 다른 방식으로 계산)할 때 조건문을 사용하지 않고 변하는 행동을 어떻게 처리할 것인가?
해결방법 : `다형적`인 메시지를 이용해서 행동이 변하는 타입들에게 각 행동을 다루기 위한 책임을 할당하라
`Protected Variations (변경 보호)`
문제 : 요소들의 변화나 불안정한 요소가 다른 요소에 해로운 영향을 미치지 않도록 할 수 있을까?
해결방법 : 변화가 예상되거나 불안정한 지점을 식별하고, 그 주위에 안정적인 `인터페이스` 또는 `추상화`를 형성하도록 책임을 할당하라
`Indirection (간접화)`
문제 : 직접적인 의존을 피하기 위해 어디에 책임을 할당해야 하는가?
해결방법 : 다른 컴포넌트나 서비스가 직접 의존하지 않도록 중재하는 중간 객체에 책임을 할당하라
해당 가이드는 도메인 로직에만 적용됩니다
도메인 로직에 속하지 않는 객체에게 책임을 할당할 때는 도메인 로직안에서 적합한 후보를 찾을 수가 없습니다.
`Pure Fabrication (순수한 가공물)`
문제 : 적당한 책임을 가진 클래스를 찾지 못하는 상황이거나 높은 응집도와 낮은 결합도를 위반하고 싶지 않은 경우에 누구에게 책임을 할당해야 하는가?
해결방법 : 도메인 개념을 표현하지 않는 인위적으로 만든 클래스에 책임을 할당하라. 이런 클래스는 높은 응집도, 낮은 결합도, 재사용을 지원하기 위해 만들어진다.
`Controller (컨트롤러)`
문제 : UI 계층을 통해 전달되는 시스템의 오퍼레이션을 전달받고 조정(제어할) 최초의 객체는 무엇인가?
해결방법 : 워크플로우를 표현하는 객체에게 책임을 할당하라
훌륭한 설계는 무엇일까?
`응집도`가 높고, `결합도`가 낮고, `캡슐화`를 지키도록 코드를 배치하는 것입니다.
즉, 변경하기 쉽게 코드를 배치하는 것
응집도
- 모듈(클래스) 내부 요소들 사이의 기능적인 집중도
- 모듈(클래스) 내부의 데이터와 메서드 간에 관련된 정도
- 응집도가 높다/낮다로 표현
- 휼륭한 설계는 응집도가 높은 설계
응집도를 높이기 위한 분리 기준
- 클래스가 하나 이상의 이유로 변경된다면 응집도가 낮은 것이다.
- `변경의 이유를 기준으로 클래스를 분리`하라
- 특정한 메서드 그룹이 특정한 속성 그룹만 사용한다면 응집도가 낮은 것이다.
- `함께 사용되는 메서드와 속성 그룹을 기준으로 클래스를 분리`하라.
- 클래스의 인스턴스를 초기화할 때 경우에 따라 서로 다른 속성들을 초기화한다면 응집도가 낮은 것이다.
- `초기화되는 속성의 그룹을 기준으로 클래스를 분리`하라.
결합도
- 모듈이 외부의 다른 모듈에 의존하는 정도
- 모듈이 다른 모듈에 대해 알고 있는 지식의 양
- 결합도가 높다/낮다, 강하다/느슨하다로 표현
- 휼륭한 설계는 결합도가 낮은 설계
결합도가 변경의 빈도와 관련이 있고 결합도를 낮추기 위해서는 `추상화에 의존하도록 코드를 배치`해야함
캡슐화
- 전통적인 관점
- 내부의 데이터와 메서드를 하나의 단위로 묶음
- 외부로부터 데이터에 대한 직접적인 접근 제한
- 공용 인터페이스를 통한 접근만 허용
변경 관점
- 변경되는 부분을 내부로 숨기는 추상화 기법
- 변경될 수 있는 어떤 것이라도 감추는 것
- 설계에서 변하는 부분이 무엇인지 고민하고 변하는 개념을 캡슐화
'WEB' 카테고리의 다른 글
[MSSQL] Partition Table(파티션 테이블) 생성 방법 (0) | 2025.02.19 |
---|---|
[WEB] 스프링 프로젝트에 Microsoft OAuth 연동하기_사전 작업 (1) | 2025.01.22 |
[Spring] 스프링 MVC에 Microsoft OAuth 연동하기 (2) | 2025.01.22 |
[Spring Boot] 스프링 시큐리티에 Microsoft OAuth 연동하기 (1) | 2025.01.15 |
[코드 리팩토링] Compose 메소드 패턴 (0) | 2024.11.20 |