아니 벌써 목요일?
class UnbelievableUserInfo {
// 이름은 null이 될 수 없음.
public String name = "홍길동";
// 계좌는 0보다 커야 함.
public int account = 10000;
// TODO: name 과 account에 부적절한 값이 할당되지 못하도록 처리하시오.
// name과 account 는 private으로 변경되어야 한다.
// END
}
public class UnbelievableTest {
public static void main(String[] args) {
UnbelievableUserInfo info = new UnbelievableUserInfo();
System.out.printf("사용자 정보:%s, %d%n", info.name, info.account);
info.name = null;
info.account = -1000;
System.out.printf("사용자 정보:%s, %d%n", info.name, info.account);
}
}
위와 같은 코드에서 이름과 계좌의 조건을 주석으로 처리해 주었다.
그럼 사용자나 해당 코드를 사용하는 다른 개발자 모두가 주석에 주의하며 사용할 수 있을까?
당연하게도 예외는 발생하며 이름에는 null, 계좌에는 -1000과 같은 요구사항에 맞지 않는 값이 들어가기 마련이다.
그럼 어떻게 해결할까?
이를 해결하기 위한 것이 바로 캡슐화
변수를 직접 설정하지 못하게 private
으로 접근을 제한하고 이를 변경하는 public
로직을 따로 제공하는 것이다.
class UnbelievableUserInfo {
// 이름은 null이 될 수 없음.
private String name = "홍길동";
// 계좌는 0보다 커야 함.
private int account = 10000;
// TODO: name 과 account에 부적절한 값이 할당되지 못하도록 처리하시오.
// name과 account 는 private으로 변경되어야 한다.
public String getName() {
return name;
}
public void setName(String name) {
if(name != null) {
this.name = name;
}else {
System.out.println("이름을 입력해주세요.\n입력값 = " + name);
}
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
if(account >= 0) {
this.account = account;
}else {
System.out.println("계좌에는 0 이상의 값만 입력 가능합니다.\n현재 입력값 = " + account);
}
}
// ENDe
}
public class UnbelievableTest {
public static void main(String[] args) {
UnbelievableUserInfo info = new UnbelievableUserInfo();
System.out.printf("사용자 정보:%s, %d%n", info.getName(), info.getAccount());
// info.name = null;
info.setName(null);
// info.account = -1000;
info.setAccount(-1000);
System.out.printf("사용자 정보:%s, %d%n", info.getName(), info.getAccount());
}
}
UnbelievableUserInfo
클래스의 멤버 변수를 private로 만들었고 해당 변수를 처리하는 getter/setter 메서드를 만들었다.
이렇게 메서드로 만들었을 때,
1. 직접 접근을 막고 데이터를 보호할 수 있다.
2. 보호를 위한 로직을 작성할 수 있다.
등과 같은 이점이 있다.
객체를 계속 생성하거나 삭제하는 것에 많이 비용이 들어서 재사용이 유리하거나,
여러 개의 객체가 필요 없는 경우에는 어떻게 할까?
위와 같은 객체를 사용하게 되면 해당 클래스를 사용하는 외부에서 new 연산자를 통해 계속 만들어지게 된다.
만약 그 클래스를 메모리에 계속 생성하는 것이 비용적인 측면에서 많은 비용이 든다면, 해당 클래스를 한 번만 생성하고 생성한 객체를 재사용하는 것이 유리하다.
이때, 사용하는 것이 싱글톤 패턴이다.
싱글톤 패턴의 특징은 상태가 없는 stateless한 객체이다.
그럼 싱글톤을 구현해보자.
class SingletonClass {
// TODO:SingletonClass에 Singleton Design Pattern을 적용하시오.
private static SingletonClass instance; // 2-1. SingletonClass의 인스턴스를 null인 상태로 생성
// private static SingletonClass instance = new SingletonClass(); // 2-2. SingletonClass가 로드될 때 바로 메모리에 객체를 생성하여 올림
private SingletonClass() {} // 1. private한 기본 생성자 생성
public static SingletonClass getInstance() { // 외부에서 해당 메서드를 통해
if(instance == null) { // SingletonClass가 없으면
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass(); // Blank static의 성질을 이용해 생성자를 통해 1회만 SingletonClass를 생성
}
}
}
return instance; // SingletonClass가 있으면 기존에 있는 or 새로 생성된 객체 반환
}
// END
public void sayHello() {
System.out.println("Hello");
}
}
public class SingletonTest {
public static void main(String[] args) {
// TODO:SingletonClass를 사용해보세요.
SingletonClass sc1 = SingletonClass.getInstance();
SingletonClass sc2 = SingletonClass.getInstance();
System.out.println(sc1 == sc2);
// END
}
}
주석에 적어놓은 번호 순서대로 작성하면 편하다.
1. 빈 생성자를 먼저 생성한다. 이때, 접근 제한자는 private으로 설정하여 외부에서 새로운 객체를 메모리에 적재하지 못하게 한다.
2-1. private한 싱글톤 객체를 null인 상태로 생성한다.
2-2. SingletonClass를 로드할 때 바로 인스턴스를 생성하고 메모리에 적재한다.
3. getInstance()메서드를 통해 인스턴스가 존재하면 기존 인스턴스를 반환하고 없으면 새로 생성하여 반환한다.
여기서 2-1과 2-2의 차이는 싱글톤 인스턴스를 생성하는 시점의 차이이다.
즉, 사용하려는 목적에 따라 상황에 맞게 구현하면 된다.
public class Polymorphism {
public static void main(String[] args) {
// 1. 다형성 변수
Object o = 123; // new Integer(123); :Auto Boxing
int i = 'A'; // int <- int, byte, short, char
float f = 123L; // 큰 집합 = 작은 집합
Person p = new Student("홍길동", 20, 202401); // 메모리에는 Student 객체까지 적재. 단, Person 객체까지만 접근 가능
p.setName("손오공"); // 가능
// p.setStuid(202477); // err
System.out.println(p.toString()); // 가능. Person 객체까지만 접근이 가능하지만, Overriding된 메서드는 접근 가능
//Teacher t = (Teacher) p; // ClassCastException : p는 이미 Student로 선언했기 때문에 형 변환 시 에러가 나옴
if(p instanceof Student s) {
// Student s = (Student) p; // local에 p가 가리키던 메모리 주소를 s에 가리키게 함. + 접근 범위를 Student까지 넓힘.
s.setStuid(202477);
}
System.out.println(p.toString());
System.out.println(p instanceof Student);
System.out.println(p instanceof Person);
System.out.println(p instanceof Object);
}
}
저 코드를 그대로 실행하면 컴파일 에러가 뜨니 하나씩 실행 or 주석 처리를 잘 하면서 사용하면 될 듯
가장 중요한 코드다.
특히, Person p = new Student("홍길동", 20, 202401);
이 줄은 Person 타입의 p변수를 Student타입으로 선언한 것이다.
무슨 말이냐면, 메모리에는 Student에 해당하는 데이터를 적재하고 접근 범위는 Person까지만 지정해 주는 것이다. (Student가 Person의 자식 클래스)
그렇기에 Student 클래스에 정의한 멤버 변수에는 접근이 불가능하다.
p.setName("손오공")
은 성공적으로 실행되지만, p.setStuid(202477)
는 에러가 나는 것.
단, 자식 클래스에 재정의(Override)한 메서드에는 접근이 가능하며 제일 우선적으로 접근하게 된다. (== 동적 바인딩)
public class Polymorphism {
public static void main(String[] args) {
// 2. 배열
int[] ia = new int[3];
ia[0] = 'A';
Person[] pa = new Person[3];
pa[0] = new Student("홍길동", 20, 202401);
pa[1] = new Teacher("김강사", 25, "자바");
pa[2] = new Employee("이사원", 30, 'A');
for(Person p : pa) p.printAll();
}
}
상속 관계에 있는 객체를 부모 타입의 배열로 한 번에 관리가 가능하다!
진짜 엄청나
또 다른 예시로,
Object[] objs = new Object[10];
objs[0] = 123;
이와 같은 코드도 가능하다.
엥, 123과 같은 기본형은 Object를 상속받지 않는데?
맞는 말이지만, Integer라는 Wrapper 클래스가 있어서 자동으로 Integer 객체로 변환되어 Object배열에 들어가게 된다.
이를 Auto Boxing이라고 함.
public class Polymorphism {
public static void main(String[] args) {
// 3. 파라미터
void set(Object p) {
...
}
set(new Student("김동열", 27, 1243021));
set(new Person("홍길동", 20));
set('A');
}
}
말 그대로 파라미터로 받는 타입에 형변환을 적용한 것
set이라는 메서드의 파라미터를 Object로 하여 모든 객체를 파라미터로 사용할 수 있게 코딩할 수 있다.
public class Polymorphism {
public static void main(String[] args) {
// 4. 리턴
Person get() {
// int get() {
return new Student("김동열", 27, 1243201);
// return 'A';// 가능
}
}
}
리턴 타입에도 형변환 개념을 적용할 수 있다.
int 타입의 get()을 작성하면 일반적으로 int만 반환해야 하지만,
Object 타입으로 작성하면 모든 객체를 반환할 수 있게 된다.
내가 개인적으로 많이 어려워 했던 내용이다.
Person person = new SpiderMan();
SpiderMan
객체가 Person
객체를 상속받고 있을 때,
메모리에는 어떻게 적재되고 person은 어디까지 접근할 수 있을까?
단 하나의 표로 해결할 수 있다.
new 연산자로 SpiderMan 객체를 생성했다. 즉, 메모리에는 SpiderMan 클래스와 관련된 정보들이 적재된다.
하지만 Person 객체에 담았기 때문에, 메모리에 SpiderMan의 정보가 있더라도 접근은 Person까지만 가능한 것.
그럼 Person 객체가 SpiderMan의 메서드를 사용하려면 어떻게 해야 할까?
바로, Person이 접근할 수 있는 범위를 늘리면 될 것이다.
그것을 도와주는 것이 명시적 캐스팅이다.
명시적 캐스팅이란, 말 그대로 직접 캐스팅 해주는 것이다.
(SpiderMan) person;
을 통해 SpiderMan의 메서드와 변수를 사용할 수 있게 된다.
그럼, person의 모든 자식으로 명시적 캐스팅이 가능할까?
그건 또 아니다.
이미 person은 SpiderMan으로 객체를 생성해서 메모리에 SpiderMan과 관련된 객체 정보까지만 올라가 있는 상태이다.
그렇기 때문에, IronMan과 같은 객체로 명시적 캐스팅이 불가능하다.
컴파일 단계에서 참조 변수의 타입에 따라 연결이 달라지는 것.
상속 관계에서 객체의 멤버 변수(static/instance)가 중복될 때 또는 static method가 있어서 컴파일 시 바로 메모리에 올라갈 때 수행.
런타임 단계에서 다형성을 이용한 메서드 호출이 발생할 때 메모리의 실제 객체 타입으로 결정하는 것.
상속 관계에서 객체의 instance method가 재정의 되었을 때 마지막으로 재정의 된 자식 클래스의 메서드가 호출됨
해당 코드를 실행할 때 나타나는 결과는?
sub
sub class method
super
sub class method
subClass의 접근은 당연하니 설명은 생략하고,
superClass의 method()는 자식 클래스에서 재정의 되었기 때문에 sub class method
가 실행되는 것.
강사님께서 다형성의 중요성과 어려움을 아시고 미리 진도를 나가주신 덕분에 오늘 라이브 강의가 크게 어렵지 않았다. 게다가 복습하는 느낌이라 오히려 기억에도 오래 남을 수 있었다.
점심시간에 신한은행 해커톤 팀원과 간단하게 회의를 했다.
아직 초반이라 구체화된 주제는 없지만 그래도 열심히 달려보도록 하자.