반응형
정리
4장 자바가 확장한 객체지향
- abstract
- 추상 클래스
- 추상 메서드 : 상위 클래스가 구체적인 기능을 구현할 수 없으나 그 기능을 구현(오버라이딩) 및 접근할 수 있도록 강제하고 싶을 때
- 생성자
- 자바 컴파일러는 생성자가 없으면 기본생성자가 자동으로 생성된다
- 생성자 = 객체 생성자 메서드
- 초기화 블록
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- 클래스가 메모리 영역에 로드되는 시점은 프로그램 실행 시가 아니라 해당 클래스를 처음 사용하는 시점임. 메모리 사용을 최대한 늦추어 효율을 높이기 위함
- 인스턴스 블록 : 인스턴스 생성 시 실행. 거의 안쓰임
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- final
- final 클래스 : 상속 불가
- final 변수 : 상수. 초기화 이후 재할당 불가
- final 메서드 : 오버라이딩 불가
- instanceof (사용 권장하지 않음)
- 해당 인스턴스의 상속, 구현중인 타입을 포함한 타입 검증 (참조변수타입과 별개)
- LSP 를 어기는 코드에서 주로 사용되므로 유의
- 이유 : 상위 타입이 하위 타입에 의존하는 코드라는 의미
→ 상속 구조가 잘못되었거나, 역할 분리가 필요할 가능성 있음- 예시
- 이유 : 상위 타입이 하위 타입에 의존하는 코드라는 의미
class Bird {
void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Penguin extends Bird {
// 펭귄은 날 수 없지만, 상속을 강제 당함
}
class BirdHandler {
void makeBirdFly(Bird bird) {
if (bird instanceof Penguin) { // ❌ LSP 위반: 특정 하위 클래스를 체크
System.out.println("펭귄은 날 수 없습니다.");
} else {
bird.fly();
}
}
}
public class Main {
public static void main(String[] args) {
BirdHandler handler = new BirdHandler();
handler.makeBirdFly(new Bird()); // "새가 날아갑니다."
handler.makeBirdFly(new Penguin()); // "펭귄은 날 수 없습니다." (LSP 위반)
}
}
- package : 네임스페이스 역할
- interface
- 멤버에 public, abstract, static final 를 자동으로 붙여주지만 명시적으로 붙여 작성하는 것을 권장
- 자바8부터 디폴트 메서드, 정적 추상 메서드 지원
- 람다 : 변수에 할당할 수 있는 함수(로직)
- 함수형 언어의 특성을 수용한 것 (자바8부터)
- 로직을 변수에 저장하고, 인자로 전달하고, 반환값으로 사용할 수 있게 함
- this : 지역변수가 아닌 객체멤버에 접근
- super : 바로 위 상위클래스에 접근
5장 SOLID -객체지향 설계원칙
- 응집도를 높이고, 결합도는 낮추라는 큰 원칙을 따름
- SRP 단일 책임 : 역할 분리. 클래스, 메서드를 변경해야 하는 이유는 단 하나일 것
- Bad case
- 하나의 속성에 상황에 따라 여러 의미를 담는 것
- if 문에 따라 다른 로직 실행
- ** 메서드에 if 문이 등장하면 한 메서드에서 여러 기능(책임)을 구현중인게 아닌지 의심해보자
- Bad case
- OCP 개방 폐쇄 : 자신의 확장에는 열려있고, 주변(외부)의 변화에는 닫혀있을 것
- 객체지향의 장점인 유연성, 재사용성, 유지보수성을 얻게 함
- 예시 모음
- 자동차 - 타이어 - 타이어종류 : 타이어는 그 종류의 확장에 열려있고, 자동차는 타이어 종류 변화에 닫혀있음
- 자바 어플리케이션 - JDBC(JDBC 드라이버) - DB종류 : DB는 그 종류 확장에 열려있고, 자바 어플리케이션은 DB종류 변경이 닫혀있음
- 자바 코드 - JVM - OS : OS는 운영체제 종류 확장에 열려있고, 자바 코드는 운영체제 변경에 닫혀있음
- LSP 리스코프 치환 : 서브타입은 항상 기반타입 인스턴스 역할을 대신할 수 있어야 함
- 상속이 분류도가 아닌 계층도로 구현되었을 때 LSP 를 위반
- e.g. 아버지 - 딸 : 아버지 춘향이 = new 딸();
- ISP 인터페이스 분리 : 역할 분리. 역할을 인터페이스로 분리하고, 각 역할에 충실한 최소한의 기능만 강제할 것
- 사용하지 않는 메서드에 의존관계가 생기지 않도록 할 것
- DIP 의존관계 역전 : 자신보다 변하기 쉬운 것에 의존하지 말 것
- 구체 클래스가 아닌 잘 변하지 않는 추상화된 것에 의존하여 변화에 영향을 적게 받도록 함
** SoC 관심사의 분리 : 하나의 대상에는 하나의 관심사만.
** SRP와 ISP 는 같은 문제(책임 중복)에 대한 두 가지 해결방안으로 볼 수 있음
일반적인 상황에서는 SRP 를 권장
- 왜 책임 중복에 대해 인터페이스보다 클래스 분리를 권장할까?
- 클래스 분리 : 책임에 대한 기능을 클래스 단위로 직접 분리하고, 구현하여 캡슐화함 → 객체 지향적
- 인터페이스 분리 : 기능을 분리할 수 있지만 결국은 책임을 지울 구현 클래스가 필요함. 인터페이스가 너무 많아지는 인터페이스 지옥에 빠질 수 있음
- "추상클래스는 물려줄 속성이 많을수록, 인터페이스는 구현할 추상메서드가 적을수록 좋다" 에 대한 답
- 추상클래스 : 하위 클래스들이 공통적으로 가지게 될 속성을 잘 뽑아내어 상위 클래스에 배치함
→ 메모리 효율 높아짐. 객체의 특성을 잘 표현했다는 의미 - 인터페이스 : 인터페이스가 맡은 역할에 충실한 최소한의 기능만 강제하는 것이 좋음(SRP)
- 추상클래스 : 하위 클래스들이 공통적으로 가지게 될 속성을 잘 뽑아내어 상위 클래스에 배치함
부가적인 내용
- 초기화 블록
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- 클래스가 메모리 영역에 로드되는 시점은 프로그램 실행 시가 아니라 해당 클래스를 처음 사용하는 시점임. 메모리 사용을 최대한 늦추어 효율을 높이기 위함
- Q. import 만 하면 어떨까? 블록 실행 안될까? 메모리 로드는?
- import 문은 클래스를 찾을 수 있도록 도와주는 역할만 함. 실행과 관련이 없음
→ 로드되지 않음!- 실행 시 로드된 클래스 확인하는 옵션 : java -verbose:class ClassLoaderTest
- import 문은 클래스를 찾을 수 있도록 도와주는 역할만 함. 실행과 관련이 없음
- JUnit 의 @BeforeAll 과 유사한 성격
- 인스턴스 블록 : 인스턴스 생성 시 실행. 거의 안쓰임
- 인스턴스 블록과 스프링 빈 라이프사이클 콜백 메서드 중 하나인 @PostConstruct 비교해보기
인스턴스 초기화 블록 {} 초기화 콜백 메서드 @PostConstruct 실행 시점 객체 생성 시 의존성 주입 후 스프링 의존성 주입 반영 불가능 가능 가독성 및 유지보수 코드 흐름에서 벗어나 위치 메서드로 분리하여 명확한 역할 분리 가능
- 인스턴스 블록과 스프링 빈 라이프사이클 콜백 메서드 중 하나인 @PostConstruct 비교해보기
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- instanceof 는 양방향이 가능하다. 실제로 담긴 객체를 기준으로 판별함
코드 실행 예제 (코드의 조건문 모두 true임)
import java.util.*;
import java.lang.*;
import java.io.*;
// The main method must be in a class named "Main".
class A {
}
class B extends A {
}
class Main {
public static void main(String[] args) {
A a = new B();
B b = new B();
if(a instanceof B){
System.out.println("a is instanceof B"); // true
}
if(b instanceof A){
System.out.println("b is instanceof A"); //true
}
System.out.println("end");
}
}
- interface 는 public 추상메서드 및 정적속성만 가질 수 있다.
- 그래서 public, abstract, static final 은 붙이지 않아도 자동으로 붙지만,
- 명시적으로 적어주는 것을 권장
- 인터페이스를 구현하는 쪽이 어디든 접근할 수 있어야 하기 때문
- 구현 클래스에서 접근제한자를 동일하거나 더 좁은 범위로 지정하여 사용
- 왜 책임 중복에 대해 ISP 보다 SRP 를 권장할까
- 클래스 분리 : 책임에 대한 기능을 클래스 단위로 직접 분리하고, 구현하여 캡슐화함 → 더 객체 지향적
- 인터페이스 분리 : 기능을 분리할 수 있지만 결국은 책임을 지울 구현 클래스가 필요함. 인터페이스가 너무 많아지는 인터페이스 지옥에 빠질 수 있음
- 추상클래스에 생성자가 왜 필요할까?
→ 하위클래스에서 공통적으로 쓸 수 있게 하기 위함!
- LSP 심화 내용을 더 살펴보자 (스터디원분의 공유내용)
- 하위형에서 선행조건은 강화될 수 없다.
- 하위 타입의 메서드는 상위 타입의 메서드보다 더 엄격한 선행조건을 가질 수 없다.
- 선행조건: 메서드가 실행되기 전에 반드시 참이어야 하는 조건
- 예시: 상위 타입 Animal의 eat(Food food) 메서드는 모든 종류의 Food 객체를 인자로 받을 수 있을 때, 하위 타입 Carnivore의 eat(Food food) 메서드가 인자로 Meat 타입만 허용한다면, 이는 선행조건을 강화한 것
- 하위형에서 후행 조건은 약화될 수 없다.
- 하위 타입의 메서드는 상위 타입의 메서드보다 더 약한 후행조건을 가질 수 없다.
- 후행조건: 메서드가 성공적으로 실행된 후에 반드시 참이어야 하는 조건
- 예시: 상위 타입 List의 get(int index) 메서드는 주어진 인덱스에 항상 존재하는 요소를 반환한다고 가정할 때, 하위 타입 ImmutableList의 get(int index) 메서드가 어떤 경우에도 null을 반환하거나 예외를 던진다면, 이는 후행조건을 약화시킨 것
- 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.
- 하위 타입은 상위 타입이 정의한 불변 조건을 반드시 만족
- 불변조건: 객체의 생명 주기 동안 항상 참이어야 하는 조건, 예시로는 Rectangle 클래스에서 "너비와 높이는 항상 양수여야 한다"
- 예시: 상위 타입 Rectangle은 "너비와 높이는 항상 0보다 크거나 같아야 한다"는 불변 조건을 가지고 있을 때, 하위 타입 Square에서 너비와 높이를 음수로 설정할 수 있도록 허용한다면, 이는 상위 타입의 불변 조건을 위반
- 그렇다면 LSP는 오바라이딩과 오버로딩을 지향하지 않는 것인가?
- AI 답변: 아닙니다. LSP(리스코프 치환 원칙)는 오버라이딩과 오버로딩을 하지 않는 것이 좋다는 의미가 아니라, 오버라이딩을 할 때 어떻게 해야 하위 타입이 상위 타입으로 안전하게 치환될 수 있는지에 대한 원칙
- 하위형에서 선행조건은 강화될 수 없다.
기본지식 짚고가기
- 추상클래스, 인터페이스는 인스턴스를 생성할 수 없다
- 오버라이딩 or 추상메서드 구현 시 접근제한자를 변경할 수 있지만,
더 넓은 범위로 변경할 수는 없음! 좁은 범위로만 가능함
반응형
'Java' 카테고리의 다른 글
[🐸객체 개구리책] 3장 자바와 객체지향 (0) | 2025.03.18 |
---|---|
[🐸객체 개구리책] 1,2장 자바 등장 배경 & 메모리 동작 방식 (1) | 2025.03.05 |
반응형
정리
4장 자바가 확장한 객체지향
- abstract
- 추상 클래스
- 추상 메서드 : 상위 클래스가 구체적인 기능을 구현할 수 없으나 그 기능을 구현(오버라이딩) 및 접근할 수 있도록 강제하고 싶을 때
- 생성자
- 자바 컴파일러는 생성자가 없으면 기본생성자가 자동으로 생성된다
- 생성자 = 객체 생성자 메서드
- 초기화 블록
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- 클래스가 메모리 영역에 로드되는 시점은 프로그램 실행 시가 아니라 해당 클래스를 처음 사용하는 시점임. 메모리 사용을 최대한 늦추어 효율을 높이기 위함
- 인스턴스 블록 : 인스턴스 생성 시 실행. 거의 안쓰임
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- final
- final 클래스 : 상속 불가
- final 변수 : 상수. 초기화 이후 재할당 불가
- final 메서드 : 오버라이딩 불가
- instanceof (사용 권장하지 않음)
- 해당 인스턴스의 상속, 구현중인 타입을 포함한 타입 검증 (참조변수타입과 별개)
- LSP 를 어기는 코드에서 주로 사용되므로 유의
- 이유 : 상위 타입이 하위 타입에 의존하는 코드라는 의미
→ 상속 구조가 잘못되었거나, 역할 분리가 필요할 가능성 있음- 예시
- 이유 : 상위 타입이 하위 타입에 의존하는 코드라는 의미
class Bird {
void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Penguin extends Bird {
// 펭귄은 날 수 없지만, 상속을 강제 당함
}
class BirdHandler {
void makeBirdFly(Bird bird) {
if (bird instanceof Penguin) { // ❌ LSP 위반: 특정 하위 클래스를 체크
System.out.println("펭귄은 날 수 없습니다.");
} else {
bird.fly();
}
}
}
public class Main {
public static void main(String[] args) {
BirdHandler handler = new BirdHandler();
handler.makeBirdFly(new Bird()); // "새가 날아갑니다."
handler.makeBirdFly(new Penguin()); // "펭귄은 날 수 없습니다." (LSP 위반)
}
}
- package : 네임스페이스 역할
- interface
- 멤버에 public, abstract, static final 를 자동으로 붙여주지만 명시적으로 붙여 작성하는 것을 권장
- 자바8부터 디폴트 메서드, 정적 추상 메서드 지원
- 람다 : 변수에 할당할 수 있는 함수(로직)
- 함수형 언어의 특성을 수용한 것 (자바8부터)
- 로직을 변수에 저장하고, 인자로 전달하고, 반환값으로 사용할 수 있게 함
- this : 지역변수가 아닌 객체멤버에 접근
- super : 바로 위 상위클래스에 접근
5장 SOLID -객체지향 설계원칙
- 응집도를 높이고, 결합도는 낮추라는 큰 원칙을 따름
- SRP 단일 책임 : 역할 분리. 클래스, 메서드를 변경해야 하는 이유는 단 하나일 것
- Bad case
- 하나의 속성에 상황에 따라 여러 의미를 담는 것
- if 문에 따라 다른 로직 실행
- ** 메서드에 if 문이 등장하면 한 메서드에서 여러 기능(책임)을 구현중인게 아닌지 의심해보자
- Bad case
- OCP 개방 폐쇄 : 자신의 확장에는 열려있고, 주변(외부)의 변화에는 닫혀있을 것
- 객체지향의 장점인 유연성, 재사용성, 유지보수성을 얻게 함
- 예시 모음
- 자동차 - 타이어 - 타이어종류 : 타이어는 그 종류의 확장에 열려있고, 자동차는 타이어 종류 변화에 닫혀있음
- 자바 어플리케이션 - JDBC(JDBC 드라이버) - DB종류 : DB는 그 종류 확장에 열려있고, 자바 어플리케이션은 DB종류 변경이 닫혀있음
- 자바 코드 - JVM - OS : OS는 운영체제 종류 확장에 열려있고, 자바 코드는 운영체제 변경에 닫혀있음
- LSP 리스코프 치환 : 서브타입은 항상 기반타입 인스턴스 역할을 대신할 수 있어야 함
- 상속이 분류도가 아닌 계층도로 구현되었을 때 LSP 를 위반
- e.g. 아버지 - 딸 : 아버지 춘향이 = new 딸();
- ISP 인터페이스 분리 : 역할 분리. 역할을 인터페이스로 분리하고, 각 역할에 충실한 최소한의 기능만 강제할 것
- 사용하지 않는 메서드에 의존관계가 생기지 않도록 할 것
- DIP 의존관계 역전 : 자신보다 변하기 쉬운 것에 의존하지 말 것
- 구체 클래스가 아닌 잘 변하지 않는 추상화된 것에 의존하여 변화에 영향을 적게 받도록 함
** SoC 관심사의 분리 : 하나의 대상에는 하나의 관심사만.
** SRP와 ISP 는 같은 문제(책임 중복)에 대한 두 가지 해결방안으로 볼 수 있음
일반적인 상황에서는 SRP 를 권장
- 왜 책임 중복에 대해 인터페이스보다 클래스 분리를 권장할까?
- 클래스 분리 : 책임에 대한 기능을 클래스 단위로 직접 분리하고, 구현하여 캡슐화함 → 객체 지향적
- 인터페이스 분리 : 기능을 분리할 수 있지만 결국은 책임을 지울 구현 클래스가 필요함. 인터페이스가 너무 많아지는 인터페이스 지옥에 빠질 수 있음
- "추상클래스는 물려줄 속성이 많을수록, 인터페이스는 구현할 추상메서드가 적을수록 좋다" 에 대한 답
- 추상클래스 : 하위 클래스들이 공통적으로 가지게 될 속성을 잘 뽑아내어 상위 클래스에 배치함
→ 메모리 효율 높아짐. 객체의 특성을 잘 표현했다는 의미 - 인터페이스 : 인터페이스가 맡은 역할에 충실한 최소한의 기능만 강제하는 것이 좋음(SRP)
- 추상클래스 : 하위 클래스들이 공통적으로 가지게 될 속성을 잘 뽑아내어 상위 클래스에 배치함
부가적인 내용
- 초기화 블록
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- 클래스가 메모리 영역에 로드되는 시점은 프로그램 실행 시가 아니라 해당 클래스를 처음 사용하는 시점임. 메모리 사용을 최대한 늦추어 효율을 높이기 위함
- Q. import 만 하면 어떨까? 블록 실행 안될까? 메모리 로드는?
- import 문은 클래스를 찾을 수 있도록 도와주는 역할만 함. 실행과 관련이 없음
→ 로드되지 않음!- 실행 시 로드된 클래스 확인하는 옵션 : java -verbose:class ClassLoaderTest
- import 문은 클래스를 찾을 수 있도록 도와주는 역할만 함. 실행과 관련이 없음
- JUnit 의 @BeforeAll 과 유사한 성격
- 인스턴스 블록 : 인스턴스 생성 시 실행. 거의 안쓰임
- 인스턴스 블록과 스프링 빈 라이프사이클 콜백 메서드 중 하나인 @PostConstruct 비교해보기
인스턴스 초기화 블록 {} 초기화 콜백 메서드 @PostConstruct 실행 시점 객체 생성 시 의존성 주입 후 스프링 의존성 주입 반영 불가능 가능 가독성 및 유지보수 코드 흐름에서 벗어나 위치 메서드로 분리하여 명확한 역할 분리 가능
- 인스턴스 블록과 스프링 빈 라이프사이클 콜백 메서드 중 하나인 @PostConstruct 비교해보기
- static 블록 : 해당 클래스가 사용되는 시점에 클래스가 static 영역에 로드되면서 실행됨
- instanceof 는 양방향이 가능하다. 실제로 담긴 객체를 기준으로 판별함
코드 실행 예제 (코드의 조건문 모두 true임)
import java.util.*;
import java.lang.*;
import java.io.*;
// The main method must be in a class named "Main".
class A {
}
class B extends A {
}
class Main {
public static void main(String[] args) {
A a = new B();
B b = new B();
if(a instanceof B){
System.out.println("a is instanceof B"); // true
}
if(b instanceof A){
System.out.println("b is instanceof A"); //true
}
System.out.println("end");
}
}
- interface 는 public 추상메서드 및 정적속성만 가질 수 있다.
- 그래서 public, abstract, static final 은 붙이지 않아도 자동으로 붙지만,
- 명시적으로 적어주는 것을 권장
- 인터페이스를 구현하는 쪽이 어디든 접근할 수 있어야 하기 때문
- 구현 클래스에서 접근제한자를 동일하거나 더 좁은 범위로 지정하여 사용
- 왜 책임 중복에 대해 ISP 보다 SRP 를 권장할까
- 클래스 분리 : 책임에 대한 기능을 클래스 단위로 직접 분리하고, 구현하여 캡슐화함 → 더 객체 지향적
- 인터페이스 분리 : 기능을 분리할 수 있지만 결국은 책임을 지울 구현 클래스가 필요함. 인터페이스가 너무 많아지는 인터페이스 지옥에 빠질 수 있음
- 추상클래스에 생성자가 왜 필요할까?
→ 하위클래스에서 공통적으로 쓸 수 있게 하기 위함!
- LSP 심화 내용을 더 살펴보자 (스터디원분의 공유내용)
- 하위형에서 선행조건은 강화될 수 없다.
- 하위 타입의 메서드는 상위 타입의 메서드보다 더 엄격한 선행조건을 가질 수 없다.
- 선행조건: 메서드가 실행되기 전에 반드시 참이어야 하는 조건
- 예시: 상위 타입 Animal의 eat(Food food) 메서드는 모든 종류의 Food 객체를 인자로 받을 수 있을 때, 하위 타입 Carnivore의 eat(Food food) 메서드가 인자로 Meat 타입만 허용한다면, 이는 선행조건을 강화한 것
- 하위형에서 후행 조건은 약화될 수 없다.
- 하위 타입의 메서드는 상위 타입의 메서드보다 더 약한 후행조건을 가질 수 없다.
- 후행조건: 메서드가 성공적으로 실행된 후에 반드시 참이어야 하는 조건
- 예시: 상위 타입 List의 get(int index) 메서드는 주어진 인덱스에 항상 존재하는 요소를 반환한다고 가정할 때, 하위 타입 ImmutableList의 get(int index) 메서드가 어떤 경우에도 null을 반환하거나 예외를 던진다면, 이는 후행조건을 약화시킨 것
- 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.
- 하위 타입은 상위 타입이 정의한 불변 조건을 반드시 만족
- 불변조건: 객체의 생명 주기 동안 항상 참이어야 하는 조건, 예시로는 Rectangle 클래스에서 "너비와 높이는 항상 양수여야 한다"
- 예시: 상위 타입 Rectangle은 "너비와 높이는 항상 0보다 크거나 같아야 한다"는 불변 조건을 가지고 있을 때, 하위 타입 Square에서 너비와 높이를 음수로 설정할 수 있도록 허용한다면, 이는 상위 타입의 불변 조건을 위반
- 그렇다면 LSP는 오바라이딩과 오버로딩을 지향하지 않는 것인가?
- AI 답변: 아닙니다. LSP(리스코프 치환 원칙)는 오버라이딩과 오버로딩을 하지 않는 것이 좋다는 의미가 아니라, 오버라이딩을 할 때 어떻게 해야 하위 타입이 상위 타입으로 안전하게 치환될 수 있는지에 대한 원칙
- 하위형에서 선행조건은 강화될 수 없다.
기본지식 짚고가기
- 추상클래스, 인터페이스는 인스턴스를 생성할 수 없다
- 오버라이딩 or 추상메서드 구현 시 접근제한자를 변경할 수 있지만,
더 넓은 범위로 변경할 수는 없음! 좁은 범위로만 가능함
반응형
'Java' 카테고리의 다른 글
[🐸객체 개구리책] 3장 자바와 객체지향 (0) | 2025.03.18 |
---|---|
[🐸객체 개구리책] 1,2장 자바 등장 배경 & 메모리 동작 방식 (1) | 2025.03.05 |