객체 지향 프로그래밍(OOP: Object Oriented Progamming)
현실에 존재하는 여러 요소, 부품들을 객체들로 먼저 만들고 이를 하나씩 조립해서 전체 프로그램을 만드는 기법으로 효율적인 코드 설계 및 작성에 최적화된 기법.
Point. OOP의 4대 기초
1. 캡슐화
2. 다형성
3. 추상화
4. 상속
Point. OOP의 3개 원칙
1. 바뀌는 부분은 캡슐화한다.
2. 상속보다는 구성을 활용한다.
3. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
들어가기 앞서서, 어쩌다 해당 4대 원칙이 나오게 되었는가?
→ "상속(Inheritance)"은 상위 객체의 메서드를 기반으로 하위 객체에서 사용, 생성 가능하게 하는 기법인데 이는 하위 객체가 전부 같은 행동을 하는 것이 아니기에 문제가 되었지만, "오버라이딩(Overiding)"을 통해 상속을 통해 받은 메서드에 대해 하위 객체에서 메서드 재정의를 통해 다른 행동으로 하도록 전환하는 기법이지만 이것도 "유지 보수"에 치명적인 문제가 있어서 "인터페이스(Interface)" 즉, 객체의 사용 방법을 정의하는 타입으로 개발 코드에서 인터페이스의 메서드를 호출하면 다른 행동을 하는 하위 객체의 재정의된 메서드가 호출하여 사용가능(상위 객체로 부터 메서드만 분리하여 정의), 메서드 이름만 알면 기능을 가져오거나 빼거나 "오버로딩(Overoding)"하여 정의하는 것이 가능, 하지만 동작을 조금 바꿀 경우, "코드 재사용, 중복 문제"가 발생하여 그때 그때 하위 객체를 전부 찾아서 코드를 일일이 고쳐야하는 문제가 발생. 그 과정에서 새로운 버그 발생 가능성 증가를 합니다.
→ 요약 : 객체 지향 프로그래밍의 핵심 개념인 상속, 오버라이딩, 인터페이스는 각각 코드 재사용과 유연성을 높여주지만, 기업 환경처럼 기능 업데이트가 잦은 경우 잦은 코드 수정과 새로운 버그 발생 가능성이라는 치명적인 유지보수 문제를 야기합니다.
기법 | 문제 |
상속 | 모든 하위 객체가 동일한 동작을 수행하지는 않음. |
오버라이딩 | 잦은 기능 변경은 유지보수가 힘듬. |
인터페이스 | 코드 재사용, 중복 문제 발생(하위 객체의 전부 찾아 개선 문제) |
디자인 원칙 : 달라지는 부분과 달라지지 않는 부분을 분리.
요점은 바뀌는 부분은 따로 뽑아서 캡슐화를 진행한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치고 확장이 가능하다.(이는 현장에서 많이 경험하여야 무엇을 바꾸고 안바꿀지를 알 수 있다.)
캡슐화를 하기 위해서는 "바뀌는 부분"과 "그렇지 않은 부분"을 분리하는 것인데요. 우선 예를 들어보죠.
→ 분리를 하기 위해서는 2개의 클래스 집합(set)을 만들어야합니다. 각 클래스 집합에는 각각의 행동을 구현하는 것을 전부 집어넣습니다. 예를들어 1. 꽥꽥거리는 행동 2. 삑삑 거리는 행동하는 클래스를 만드는 것처럼요.
디자인 원칙 : 구현보다는 인터페이스에 맞춰서 프로그래밍.(상위 형식에 맞춰서 구현)
각 행동은 인터페이스로 표현하고 이런 인터페이스를 사용해서 행동을 구현하겠습니다. "특정 행동" 만을 목적으로 하는 클래스의 집합을 만들겠습니다. 인터페이스는 Duck 클래스가 아닌 행동 클래스에서 구현합니다.
전에 쓴 방법과 다른 점은 전꺼는 항상 특정 구현에 의존했기에 코드를 추가하지 않는 이상 행동을 변경할 여지가 없었지만, 새로운 디자인을 사용하면 Duck의 하위 객체들은 FlyBehavior, QuackBehavior로 표현되는 행동을 사용.
→ 요약 : 이제부터 Duck의 행동은(특정 행동 인터페이스를 구현한) 별도의 클래스 안에 들어있음. 그러면 Duck 클래스에서는 그 행동을 구체적으로 구현할 필요가 없다.
→ 구현(Implementation)에 맞춰 프로그래밍하지 말고, 인터페이스(Interface)에 맞춰 프로그래밍해라.
[예시]
나쁜 예시 : 구현에 맞춰 프로그래밍
이 방식은 Duck 클래스가 특정 행동 클래스의 존재를 직접 알고 있는 경우입니다. 다시 말해, Duck 클래스 내부에 특정 행동 객체를 직접 생성하여 사용하는 방식이죠.
// 1. '날기' 행동을 구현한 구체적인 클래스 class FlyWithWings { public void fly() { System.out.println("날개로 훨훨 납니다!"); } } // 2. 오리 클래스가 '날기' 행동을 직접 사용(생성) class MallardDuck { private FlyWithWings flyAction = new FlyWithWings(); // <-- 특정 구현 클래스에 의존 public void performFly() { flyAction.fly(); } }
문제점 : 만약 MallardDuck이 로켓 추진기로 날도록 행동을 바꾸고 싶다면, MallardDuck 클래스 내부 코드를 직접 수정해서 new FlyWithRocket()으로 바꿔야 합니다. 이거는 유연성이 없죠?
좋은 예시 : 인터페이스에 맞춰 프로그래밍
이 방식은 Duck 클래스가 특정 행동 클래스가 아닌, 행동을 정의하는 '인터페이스'에만 의존하는 경우입니다. 실제 행동을 담고 있는 객체는 외부에서 주입받는(넘겨받는) 형태가 됩니다.
// 1.'날기' 행동을 정의하는 인터페이스 interface FlyBehavior { void fly(); } // 2.인터페이스를 구현하는 구체적인 행동 클래스들 class FlyWithWings implements FlyBehavior { // 구현 클래스 public void fly() { System.out.println("날개로 훨훨 납니다!"); } } class FlyNoWay implements FlyBehavior { // 구현 클래스 public void fly() { System.out.println("저는 날 수 없어요."); } } // 3.오리 클래스가 '인터페이스'에 맞춰 프로그래밍 / 추상 클래스 class Duck { FlyBehavior flyBehavior; //특정 구현이 아닌 '인터페이스' 타입으로 변수 선언 public void setFlyBehavior(FlyBehavior flyBehavior) { //행동을 '주입'받는 메소드 this.flyBehavior = flyBehavior; } public void performFly() { flyBehavior.fly(); //인터페이스를 통해 메소드 호출 } }
public class Main { public static void main(String[] args) { Duck mallard = new Duck(); mallard.setFlyBehavior(new FlyWithWings()); // 날개로 나는 행동 주입 mallard.performFly(); // "날개로 훨훨 납니다!" // 실행 중에 행동을 변경할 수 있습니다! mallard.setFlyBehavior(new FlyNoWay()); // 날지 못하는 행동으로 변경 mallard.performFly(); // "저는 날 수 없어요." } }
Duck 클래스가 FlyBehavior라는 인터페이스만 알고 있습니다. 인터페이스가 정의하는 fly() 메소드가 호출될 것이라는 사실만 알 뿐, 어떤 클래스가 그 행동을 구현했는지는 전혀 신경 쓰지 않는다는 점 기억!!
그리고, Duck 클래스에 행동(날기)을 설정하는 메소드(Setter Method)를 추가하면, 프로그램이 실행 중일 때도 MallardDuck의 나는 행동을 마음대로 바꿀 수 있음.
여기서 인터페이스의 실제 핵심 역할은 프로그램 실행 도중에 / 쓰이는 객체가 코드에 / 고정되지 않고 / 상위형식에 맞춰서 프로그래밍해서 / 다형성을 활용해야 한다는 점.이 포인트라고 할 수 있습니다.
대부분의 상위 형식은 추상 클래스나 인터페이스같은 건데 이는 객체를 변수에 대입할 때 상위 형식으로 구현된 형식이라면 대부분의 객체를 넣을 수 있습니다. 그렇기에 변수를 선언하는 클래스에서 실제 객체의 형식은 몰라도 된다.
상속(Inheritance)
"상위 객체의 메서드를 기반으로 하위 객체에서 사용"하고, "하위 객체가 전부 같은 행동을 하는 것이 아니기에 문제가 된다."
추상화(Abstraction)
복잡한 것을 숨기고 핵심만 드러내는 것.( 객체 간의 결합도를 낮추고 코드를 더욱 유연하고 확장 가능)
FlyBehavior라는 인터페이스는 "나는 행동"이라는 "추상적인 개념"만을 정의하고
Duck 클래스는 이 FlyBehavior라는 추상화된 개념에만 "의존 정의한 것"이 바로 추상화의 핵심.
캡슐화(Encapsulation)
"달라지는 부분과 달라지지 않는 부분을 분리. 바뀌는 부분은 따로 뽑아서 캡슐화를 진행한다"라는 문장이 바로 캡슐화의 정의. '날기' 행동을 FlyBehavior라는 별도의 인터페이스와 클래스로 묶어 분리한 것이 캡슐화의 완벽한 예시입니다.
다형성(Polymorphism)
"개발 코드에서 인터페이스의 메서드를 호출하면 다른 행동을 하는 하위 객체의 재정의된 메서드가 호출"된다는 설명이 바로 다형성을 이야기하는 것입니다. 즉, 한 문장으로 여러 문장을 시킬 수 있는 것이 다형성.
'IT개발 > Java' 카테고리의 다른 글
[Java, OCJA] QUESTION 84, What is the result? (1) | 2025.01.27 |
---|---|
[Java] 2차원 배열 (0) | 2025.01.23 |
[Java]내가 생각하는 중요한 자바 용어 정리2 (0) | 2024.10.29 |
[Java] IOException 발생원인(eclipse) (0) | 2024.09.28 |
[Java]내가 생각하는 중요한 자바 용어 정리 (2) | 2024.09.28 |