Java - OOP 캡슐화

Kwon Yongho·2023년 12월 18일
0

Java

목록 보기
5/6
post-thumbnail

캡슐화(Encapsulation)

  • 캡슐화는 객체 내부의 속성이나 행위를 외부에서 직접 접근할 수 없게 하는 것이다. 흔히 은닉과 혼동하는 경우가 많은데, 은닉화란 캡슐화를 통해 얻어지는 '실제 구현 내용 일부를 외부에 감추는 효과'이다.
  • 캡슐화는 정보 은닉 개념중 하나이다.

1.목적

  • 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
  • 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출

2.구현 방법

대표적으로 2가지가 있습니다.

  • 접근제어자(access modifiers) 활용
  • getter/setter 메서드 활용

자바에는 총 4가지의 접근 제어자가 존재합니다.

  • 접근 제어자의 접근 범위가 각각 클래스 내, 패키지 내, 다른 패키지의 하위 클래스, 그리고 패키지 외까지 각각 다른 것을 확인할 수 있습니다.

간단한 예제

public class SomeClass {
	private int id;
	private String name;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	// setter 메소드에서 데이터 유효성 체크
	public void setName(String name) {
		// null check
		if(name == null) this.name = "test";
		// length check
		else if(name.length() > 5) this.name = name.substring(5);
		// else
		else this.name = name;
	}
}
  • private 선언을 통해 내부 필드에 직접 접근할 수 없게 했다. 그리고 public getter/setter 메소드를 통해 내부 필드에 접근할 수 있게 했다.
public class Main {
	public static void main(String[] args) {
		SomeClass some = new SomeClass();

		some.setId(123);
		some.setName("some");

		System.out.println(cap.getId());
		System.out.println(cap.getName());
    }
}
  • 메인 클래스에서 사용할 때 getter/setter 메소드를 통해 내부 필드에 접근한다. 이렇게 내부 필드 혹은 메소드에 직접 접근할 수 없게 하고 정보를 은닉하면 클래스 내부에 구현된 코드에 따라 목적에 맞게 동작할 가능성이 올라간다.
  • 장점의 예로 사용자가 내부 필드에 접근하는 방법을 public 메소드로 한정하고, 접근 시 유효성 체크를 public 메소드에서 처리할 경우 내부 필드 데이터 적합성을 향상시킨다.

정보 은닉

  • 정보 은닉을 구체적으로 말하면, 객체지향 언어적 요소를 활용하여 객체에 대한 구체적인 정보를 노출시키지 않도록 하는 기법을 칭한다.
  • 디자인 패턴과 같이 좋은 설계를 대표 할만한 것들은, 사실 거슬러 올라기자면 정보 은닉 기법을 통해 얻는 이득을 극대화 하기 위해 만들어진 것들이다.

정보 은닉의 목적

  • 코드가 구체적인 것들(타입, 메소드, 구현)에 의존하는 것을 막아줌으로써 객체 간의 구체적인 결합도를 약화시켜 기능의 교체나 변경이 쉽도록 함.
  • 동일한 타입의 다른 구현 객체들을 교체함으로써 동적 기능 변경이 가능함.
  • 연동할 구체적인 구현이 없는 상태에서도 (인터페이스 만으로) 정확한 연동 코드의 생성이 가능함.

대표적인 정보은닉

  • 객체의 구체적인 타입 은닉 (업캐스팅)
  • 객체의 필드 및 메소드 은닉 (캡슐화)
  • 구현 은닉 (인터페이스 & 추상 클래스)

1. 객체의 타입 은닉 (업캐스팅)

  • 부모 객체의 타입으로 형변환 하는 업캐스팅도 일종의 정보 은닉이다. 구체적인 자식 객체의 타입을 은닉함으로써 얻는 효과가 있기 때문이다.
// 동물을 나타내는 추상 클래스
abstract class Animal {
    abstract public void action();
}

class Dog extends Animal {
    @Override
    public void action() {
        makeSound();
    }

    private void makeSound() {
        System.out.println("Woof, woof!");
    }
}

class Cat extends Animal {
    @Override
    public void action() {
        climb();
    }

    private void climb() {
        System.out.println("Climbing a tree");
    }
}

public class Main {
    public static void main(String[] args) {
        // 추상화를 통한 업캐스팅
        Animal animal = new Dog();
        animal.action(); // Output: Woof, woof!

        animal = new Cat();
		animal.action(); // Output: Climbing a tree
    }
}
  • Dog, Cat은 업캐스팅되어 action 메소드로만 사용이 제한되게 되었다.
  • 해당 코드를 살펴 보면 각각 동물의 makeSound(), climb()은 main 클래스에서 직접 호출되지 않게 됩니다. (객체 타입과 메서드 은닉)

이러한 정보 은닉으로 얻는점은 뭘까?

  • Animal 클래스 생성으로 DogCat 에 의존하지 않는다.
  • 변수 할당문만 변경 시 코드 수정 할 필요가 없다.

1-1. 디자인 패턴 추가 (Factory 패턴)

조금 더 캡슐화 하기 위해 디자인 패턴을 추가하였습니다.

// 동물을 나타내는 추상 클래스
abstract class Animal {
    abstract public void action();
}

// 개를 나타내는 클래스 (Animal을 상속)
class Dog extends Animal {
    @Override
    public void action() {
        System.out.println("Woof, woof!");
    }
}

// 고양이를 나타내는 클래스 (Animal을 상속)
class Cat extends Animal {
    @Override
    public void action() {
        System.out.println("Climbing a tree");
    }
}

// Animal을 생성하는 팩토리 클래스
class AnimalFactory {
    public static Animal createDog() {
        return new Dog();
    }

    public static Animal createCat() {
        return new Cat();
    }
}

public class Main {
    public static void main(String[] args) {
        // Factory 패턴을 통한 객체 생성
        Animal animal = AnimalFactory.createDog();
        animal.action(); // Output: Woof, woof!

        animal = AnimalFactory.createCat();
        animal.action(); // Output: Climbing a tree
    }
}
  • 구체적인 자식 객체를 생성하는 AnimalFactory 클래스를 구현함으로써 DogCat 클래스에 대한 구체적인 객체에 대한 정보를 은닉하였습니다.

2. 객체의 필드 & 메소드 은닉 (캡슐화)

필드 + 메소드 은닉에 대해서 알아보겠습니다.

아래 코드는 Person 클래스 구현으로 나이와 이름 행동을 나타내는 클래스 입니다.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        if (age >= 0) {
            this.age = age;
        } else {
            System.out.println("나이는 음수일 수 없습니다.");
        }
    }

    // 행동을 나타내는 메소드
    public void eat() {
        System.out.println(name + "이(가) 식사합니다.");
    }

    public void sleep() {
        System.out.println(name + "이(가) 잠을 잡니다.");
    }

    public void work() {
        System.out.println(name + "이(가) 일합니다.");
    }

    // 객체의 정보를 출력하는 메소드
    public void introduce() {
        System.out.println("안녕하세요, 제 이름은 " + name + "이고, 나이는 " + age + "살입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Person 객체 생성
        Person person = new Person("John", 30);

        // 캡슐화된 메소드를 통한 동작 수행
        person.introduce();
        person.eat();
        person.sleep();
        person.work();
    }
}
  • 위 Main 클래스를 살펴보면 모두 Person 클래스의 메소드들을 의존하게 됩니다.
  • 해당 메소드들을 private화 하고 performDailyRoutine를 public으로 추가하여 메소드들을 은닉 하도록 하겠습니다.
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        if (age >= 0) {
            this.age = age;
        } else {
            System.out.println("나이는 음수일 수 없습니다.");
        }
    }

    // private 메소드로 변경된 행동을 나타내는 메소드들
    private void eat() {
        System.out.println(name + "이(가) 식사합니다.");
    }

    private void sleep() {
        System.out.println(name + "이(가) 잠을 잡니다.");
    }

    private void work() {
        System.out.println(name + "이(가) 일합니다.");
    }

    // public 메소드를 통해 private 메소드 실행
    public void performDailyRoutine() {
        introduce();
        eat();
        sleep();
        work();
    }

    // 객체의 정보를 출력하는 메소드
    private void introduce() {
        System.out.println("안녕하세요, 제 이름은 " + name + "이고, 나이는 " + age + "살입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Person 객체 생성
        Person person = new Person("John", 30);

        // public 메소드를 통해 private 메소드 실행
        person.performDailyRoutine();
    }
}
  • 은닉 함으로써 메소드의 중요성도 표현 할 수 있고 객체의 세부 내용을 덜 노출 시키게 되었습니다.

3. 구현 은닉 (인터페이스)

  • 위 처럼 클래스 구성 시 의존성은 오직 공개 메소드에 의해서만 발생되게 됩니다.
  • 공개 메소드를 은닉 메소드들과 구분하고 통합적으로 관리하기 위해서, 자바에서는 클래스와 유사하게 상속 가능한 타입이면서 구체적인 구현을 베제한 인터페이스(Interface)를 만들어 메소드 추상화를 통해 상속 시킬 공개 메서드를 통합적으로 관리하게 하였습니다.
// 행동을 나타내는 인터페이스
interface PersonInterface {
    void performDailyRoutine();
}

public class Person implements PersonInterface {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        if (age >= 0) {
            this.age = age;
        } else {
            System.out.println("나이는 음수일 수 없습니다.");
        }
    }

    // private 메소드로 변경된 행동을 나타내는 메소드들
    private void eat() {
        System.out.println(name + "이(가) 식사합니다.");
    }

    private void sleep() {
        System.out.println(name + "이(가) 잠을 잡니다.");
    }

    private void work() {
        System.out.println(name + "이(가) 일합니다.");
    }

    // 인터페이스를 구현한 메소드
    @Override
    public void performDailyRoutine() {
        introduce();
        eat();
        sleep();
        work();
    }

    // 객체의 정보를 출력하는 메소드
    private void introduce() {
        System.out.println("안녕하세요, 제 이름은 " + name + "이고, 나이는 " + age + "살입니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 다형성을 통한 객체 생성
        PersonInterface person = new Person("John", 30);

        // 인터페이스를 통해 메소드 실행
        person.performDailyRoutine();
    }
}
  • 인터페이스를 이용하면서 실질적 클래스 간의 의존 관계가 없어지면서 기능 확장에 있어 제약이 줄어들게 되었습니다.

0개의 댓글