자바 기초 03 - 클래스/상속/오버로딩/인터페이스/추상화/SRP-DIP
유투버 '데어프로그래밍'님 강의 참조
01 클래스
- 클래스는 상태(필드)와 행위(메소드)의 개념으로 정의 된다.
자동차 클래스(필드)
상태:
Color = 파란색
Name = 소나타
Brand = 현대
Power = 2000
Speed = 0
- 클래스의 상태는 변할 수 있는게 있고 (Speed) 변하지 못하는것이 있다 (이름/브랜드 등)
- 바뀔 수 있는 상태의 변수들은 어떠한 행위가 이루어지면 바뀔 수 있다 (예: 메소드를 통한 행위). 즉 여기에서 자바에서 가장 중요한 '객체지향(OOP)'개념이 나온다.
첫번째 객체지향 중요 개념 - 상태는 스스로 변하는게 아니라 행위에 의해서 변한다 다시말해 클래스의 필드는 메서드에 의해서 변한다 - 또한 원인없이 상태가 변하면 잘못된 프로그래밍이다
행위(메서드)
엑셀() {
speed +=;
}
브레이크() {
speed -=;
}
예를 통해서 보자
class Player{
String name;
private int thirsty; //(0~100) 3번 시나리오를 위해 'private' 추가
public Player(String name, int thirsty) {
this.name = name;
this.thirsty = thirsty;
}
//원인을 위한 메소드
void Drink() {
System.out.println("Drank water");
this.thirsty = this.thirsty - 50;
}
int thirstyStatus() {
return this.thirsty;
}
}
public class OOPEx01 {
public static void main(String[] args) {
Player p1 = new Player("Kim", 100);
System.out.println("Name: "+p1.name);
System.out.println("thirst : "+p1.thirstyStatus());
//1. 첫번째 시나리오 = 원인없이 상태 변경 (x)
//p1.thirsty = 50;
//2. 원인을 통해 상태가 행위를 변경함, 하지만 실수 가능성이 있는 코드
//p1.Drink();
//System.out.println("thirst : "+p1.thirsty);
//3.private을 걸어 변수 접근 차단
p1.Drink();
System.out.println("thirstyStatus: "+p1.thirstyStatus());
}
}
Name: Kim
thirst : 100
Drank water
thirstyStatus: 50
- 객체지향 프로그래밍에서는 '상태는 행위를 통해 변한다'
- 누군가는 상태를 직접 변경 할 수 있기 때문에 직접 접근을 하지 못하게 'private'을 쓰자
- 접근 제어자 + 적절한 메소드를 수반하여 원인을 통해 행위가 이루어지면서 상태가 변해야 한다
02 상속
- 'extends'로 쓰이고 상속(확장)한다는 개념으로 보면 된다.
- 상속은 '추상화' 가능, '상태/행위'를 가져와서 쓸 수 있으므로 엄청나게 편리하다 (상속시에는 타입이 일치해야 한다. 쉽게 말해 자동차는 엔진을 상속할 수 없지만 치즈햄버거는 햄버거를 상속 할 수 있는 개념)
- 꼭 굳이 상속을 안해도 된다. 아래 두코드를 비교해보면 컴포지션으로도 가능하고 상속해서도 가능하다.(편한걸 쓰자)
- 타입이 다른경우 상속을 하지 못하지만 불러와서 쓸 수는 있다. (콤포지션이라고 부름)
class Engine {
int power = 2000;
}
class Car {
Engine e;
public Car(Engine e) {
this.e = e;
}
}
public class OOPEx02 {
public static void main(String[] args) {
Engine e1 = new Engine();
Car c1 = new Car(e1);
System.out.println("Power: " + c1.e.power);
}
}
Power: 2000
- 상속시에는 상속하는 클래스의 모든 변수 및 기능을 가져와 쓸 수 있고, 자기 클래스에 선언된것을 먼저 적용하게 된다. 또한 변수 및 메소드는 따로 정의 안해도 쓸 수 있다. (나중에 Overriding을 배우면 가져오는 메소드를 변형 해서 쓸 수 있다)
class Hamburger {
String name = "Hamburger";
String topping = "cavage";
String topping2 = "patty";
}
//상속시에 상태/행위를 물려받게되고 타입이 일치 해야 한다
class CheeseBurger extends Hamburger {
String name = "Cheese";
}
public class OOPEx02 {
public static void main(String[] args) {
CheeseBurger ch1 = new CheeseBurger();
System.out.println("Name: " + ch1.name);
System.out.println("Topping: " + ch1.topping);
System.out.println("Topping: " + ch1.topping2);
}
}
Name: Cheese
Topping: cavage
Topping: patty
03 오버로딩
- 가장 기본적인 오버로딩 예를 보자
class 전사 {
String name = "전사";
void 기본공격(궁사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
}
class 궁사 {
String name = "궁수";
void 기본공격(광전사 e1) {
System.out.println("활로 "+e1.name+" 공격");
}
}
class 광전사 {
String name = "광전사";
void 기본공격(전사 e1) {
System.out.println("도끼로 "+e1.name+" 공격");
}
}
public class OOPEx03 {
public static void main(String[] args) {
전사 u1 = new 전사();
궁사 u2 = new 궁사();
광전사 u3 = new 광전사();
u1.기본공격(u2);
u2.기본공격(u3);
u3.기본공격(u1);
}
}
→ 이렇게 되면 각각의 클래스가 가진 기본공격에는 지정되어있는 다른클래스밖에 넣지 못한다. 해결은 가능하다 아래처럼
class 전사 {
String name = "전사";
void 기본공격(궁사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
void 기본공격2(광전사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
}
→ 가장 치명적인 단점 및 해서는 안될 코드이다. 공격 대상이 늘어나면 기본공격 메소드도 계속해서 늘어나야 한다. 이때 쓰는게 오버로딩이다.
class 전사 {
String name = "전사";
void 기본공격(궁사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
void 기본공격(광전사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
}
public class OOPEx03 {
public static void main(String[] args) {
전사 u1 = new 전사();
u1.기본공격(u3);
→ 이렇게 편하게 하나의 메소드 이름으로 통일하여 오버로딩을 할 수 있다. 하지만 여기에도 치명적인 단점이 있다.
class 전사 {
String name = "전사";
void 기본공격(궁사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
void 기본공격(광전사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
void 기본공격(엘프 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
void 기본공격(흑마법사 e1) {
System.out.println("검으로 " +e1.name+" 공격");
}
}
class 궁사 {
String name = "궁수";
void 기본공격(광전사 e1) {
System.out.println("활로 "+e1.name+" 공격");
}
}
class 광전사 {
String name = "광전사";
void 기본공격(전사 e1) {
System.out.println("도끼로 "+e1.name+" 공격");
}
}
class 마법사 {
String name = "마법사";
void 기본공격(전사 e1) {
System.out.println("마법으로 "+e1.name+" 공격");
}
}
class 엘프 {
String name = "엘프";
void 기본공격(전사 e1) {
System.out.println("표창으로 "+e1.name+" 공격");
}
}
class 흑마법사 {
String name = "흑마법사";
void 기본공격(전사 e1) {
System.out.println("마법검으로 "+e1.name+" 공격");
}
}
public class OOPEx03 {
public static void main(String[] args) {
전사 u1 = new 전사();
궁사 u2 = new 궁사();
광전사 u3 = new 광전사();
엘프 u4 = new 엘프();
흑마법사 u5 = new 흑마법사();
u1.기본공격(u2);
u2.기본공격(u3);
u3.기본공격(u1);
u1.기본공격(u3);
u1.기본공격(u4);
u1.기본공격(u5);
}
}
→ 오버로딩은 메소드를 통일해서 편하게 가져다 쓸 수 있지만 이렇게 객체가 가히 급수적으로 늘어나면 한도 끝도 없이 계속해서 만들어 줘야 한다 (유닛 100개 = 기본공격 100개)
→ 오버로딩은 어느정도 경우의 수에 제한이 있으면 편하다. 경우의 수가 많다면 오버로딩의 한계는 분명하다. 가장 간단하게 해결하는 방법은 상속을 받아서 오버라이딩(재정의)해서 쓰는게 가장 베스트이다. (따로 선언도 안해도되고 필요기능만 수정해서 쓰면 된다.)
04 인터페이스 / 추상화
- 행위(메서드,변수바꿈 등)에 대한 강제성 및 제약을을 부여해 인터페이스를 쓰고자하는 사람은 강제된 변수, 기능들을 바꾸지 못하고 그대로 가져와서 써야 한다. 그로인해 통일성 및 유지보수에 엄청난 장점이 있다.
- 인터페이스 구현시에는 모든 변수와 메서드는 추상적으로 만들어 놓아야 한다. (추상적이라는 말은 명확하게 선언이 되어있는것이아닌 정말 두리뭉술하게 큰 그림의 형식으로 선언)
interface MoveAble {
//public abstract 생략
void up();
void down();
void left();
void right();
}
interface MoveAble2 {
void up();
void down();
void left();
void right();
void hide();
}
abstract class violentAnimal implements MoveAble{
abstract void attack();
@Override
public void up() {
System.out.println("Up");
}
@Override
public void down() {
System.out.println("Down");
}
@Override
public void left() {
System.out.println("Left");
}
@Override
public void right() {
System.out.println("Right");
}
}
abstract class gentleAnimal implements MoveAble2{
abstract void harvest();
@Override
public void up() {
System.out.println("Up");
}
@Override
public void down() {
System.out.println("Down");
}
@Override
public void left() {
System.out.println("Left");
}
@Override
public void right() {
System.out.println("Right");
}
@Override
public void hide() {
System.out.println("Hide");
}
}
//interface를 받은 후 부모가 구현안하면 자식이 해야함, 그러면 코드가 더 길어짐
class Monkey extends gentleAnimal implements MoveAble{
@Override
void harvest() {
System.out.println("Harvesting bananas");
}
@Override
public void up() {
}
@Override
public void down() {
}
@Override
public void left() {
}
@Override
public void right() {
}
}
class Cow extends gentleAnimal {
@Override
void harvest() {
System.out.println("Harvesting grass");
}
}
class Tiger extends violentAnimal{
@Override
void attack() {
System.out.println("Attacking with teeth");
}
}
class Rhino extends violentAnimal{
@Override
void attack() {
System.out.println("Attacking with body");
}
}
public class OOPEx04 {
static void Controller(gentleAnimal g) {
g.harvest();
g.hide();
g.up();
g.down();
g.left();
g.right();
System.out.println("================");
}
static void Controller(violentAnimal g) {
g.attack();
g.up();
g.down();
g.left();
g.right();
System.out.println("================");
}
public static void main(String[] args) {
Cow c1 = new Cow();
Controller(c1);
Monkey m = new Monkey();
Controller(m);
Tiger t = new Tiger();
Controller(t);
}
}
- 부모 클래스에서 추상화/인터페이스를를 통해 넓은 그림의 메소드를 정의하고 그 기능에대한 강제성 및 제약조건을 자식에게 부여하여서 통일성을 가추도록 한다
- 자식은 부모의 추상화와 인터페이스를 받아 코드의 통일성을 가지되 자기 함수 안의 내용은 자기가 원하는데로 바꿀 수 있다
- Controller() 함수를 보면 부모를 매개변수로 받아 부모를 상속하고있는 자식들을 끌어다 쓸 수 있는것도 보인다. 여기서 중요한것은 부모를 매개변수로 쓰면 다형성이 커지고 코드 자체가 쉬워지지만 단순 Monkey같은 특정 메소드를 파라미터로 받아오면 다른것들은 불러 올 수 없다.
- 여기서 다시 메소드를 추가해도 엄청나게 쉬워 진다.
class Horse extends gentleAnimal {
@Override
void harvest() {
System.out.println("eating grass");
}
}
public class OOPEx04 {
static void Controller(gentleAnimal g) {
Horse h = new Horse();
Controller(h);
}
eating grass
Hide
Up
Down
Left
Right
→ 중요한 포인트는 항상 부모클래스에서 대부분의 코드를 짜놓고 자식들은 쉽게 받아서 쓴다는 형태로 기억 하자
→ 추상화는 미완성된 메서드(설계도), 행위에 대한 제약은 인터페이스로 해야 된다.
05 SRP/DIP
- SRP - Single Response Principle (단일 책임 원칙) 의 약자로 객체는 단 하나의 책임만 가져야 한다는 의미. 여기서 책임은 해야하는 것 혹은 할 수 있는 것으로 보면 되고 쉽게 말해 책임을 분리하여 각각의 기능들이 분명한 기능들을 가지고 있음으로 인해 어떠한 책임이 변경되더라도 다른 책임들에게는 영향이 가지 않는 것. 즉 하나의 클래스가 너무 많은 기능을 가지고 있으면 어떠한 기능이 변경되면 그 클래스 전부를 테스트해야하는 번거로움이 생김. 이때 단순 깔끔하게 SRP 원칙으로 개발하면 유지보수에 탁월
- DIP - Dependency Inversion Principle (의존 역전 원칙) 의 약자로 객체간의 의존성을 주입, 자주 변화하는 것들은 거의 변화하지 않는것들에게 의존하라는 원칙. 즉 자주 안바뀐다는말은 부모클래스를 생각하면 쉽고 부모클래스 혹은 추상클래스를 만들거나 인터페이스등을 만들어서 주입하면 DIP가 강안 유연한 시스템 설계가 된다.
- 더 많은 원칙들은 다음 링크를 참조 (https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=1ilsang&logNo=221105781167)
- 이 두가지 원칙을 보면 '처음에 완벽하게 만들면 되지않을까?' 라고 생각 할 수 있지만, 절대로 처음부터 완벽하게 만들 수 없다. 그래서 항상 프로그래밍에서는 'CI(Continuous integrity)' 지속적 통합을 지향하여 하나의 프로그램은 처음부터 끝까지 계속해서 발전하고 업데이트가 되어야 서서히 완벽에 가까운 프로그램이 나오고, 이러한 원칙을 기반으로 짜지 않으면 나중에 수정이 엄청나게 어려워 지는 단점이 있다. 결국에는 모든 관점, 원칙, 기반 이러한 것들은 유지보수에 어떻게 최적화가 되는가에 초점이 맞춰져 있다.