제어자
이제부터 객체지향에서 어려운 부분이 시작된다. 다형성이나 추상화를 알기 위해서는 이번 파트에 나오는 용어들에 대해서 자세히 알아야 이해를 할 수가 있다.
제어자는 클래스와 클래스의 멤버에 부가적인 의미를 부여하는 것이다. 즉, 영어에서의 형용사와 같다. 종종 제어자가 메서드나 변수에만 붙는 다고 생각할 수 있는데, 제어자는 클래스에도 붙을 수 있다는 것을 유의하고 들어가자.
제어자에는 두 가지가 있다.
- 접근 제어자
- 접근 제어자는 한 개 밖에 사용하지 못하고, 어떠한 접근 권한을 주는 제어자이다.
- public, protected, (default), private 가 있고, 기본 값이 default라서 따로 선언하지 않으면 default로 지정된다.
- 그 외
- static, final, abstract, native, transient, synchronized, volatile, strictfp
되게 많은 것을 볼 수 있다. static, final, abstract는 많이 사용되고, 그나마 사용되는 것을 본 것은 동기화를 위한 synchronized 인데, 다른 제어자도 나중에 자세히 살펴보자.
여기서 중요한 것은 제어자는 접근 제어자를 제외하고는 여러 가지를 같이 쓸 수 있다. 가령 이런 것도 가능하다는 것이다. 다만 주의해야 할 점은 각 제어자마다 사용할 수 있는 범위가 있다. 메서드에 사용할 수 없는 제어자도 있을 것이고, 변수에 사용할 수 없는 제어자도 있다는 말이다.
public static final transient ... varible;
사실 이렇게 많이 제어자를 쓸 일은 없겠지만 가능하다는 것만 알아두자. 그리고 접근 제어자를 맨 앞에 쓴 것을 볼 수 있는데, 접근제어자와 다른 제어자의 순서가 바뀌어도 문제는 없지만 관행상 접근 제어자를 맨 앞에 쓴다. 개발은 커뮤니케이션과 통일성이 중요하기 때문에 웬만하면 맨 앞에 쓰도록 하자.
static
먼저 static 부터 살펴보자. 예전에도 static 을 공부했었던 것 처럼 static은 변수 및 메서드에 설정이 가능하다. 위에서도 얘기했지만 제어자 중에는 클래스에 붙을 수 도 있는 것도 있기때문에 static만 보고 변수 메서드만 가능하구나 라는 생각은 버려야 한다. 추가적으로 static 은 초기화 블럭에도 사용할 수 있다는 것도 알고 넘어가자.
final
이전에 상수를 등록하기 위해 사용했던 final 제어자이다. static과 달리 final은 클래스에서도 사용 할 수 있는데, 그 특징이 익숙하지 않을 가능성이 높기 때문에, 해당 제어자에 대해서는 간단하게 정리를 할 필요가 있다.
클래스 변경될 수 없는 클래스, 확장 될 수 없는 클래스가 된다. 따라서 final 키워드가 붙은 클래스는 조상이 될 수 없다.
메서드 | 변경될 수 없는 메서드이다. 따라서 오버라이딩이 불가능 하다. |
멤버 변수 | 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다. |
지역 변수 | 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다. |
우선 클래스부터 살펴보자. 클래스 앞에 final 이 붙는다는 것은 말 그대로 다음과 같다.
final class Const {}
해당 클래스는 조상이 될 수 없다. 그리고 변경될 수 없는 클래스라고 적혀있는 것을 볼 수 있는데, 몇 가지 궁금한 부분이 생겨서 테스트를 한 번 해봤다.
public final class Const {
static public int WIDTH = 200;
public int k = 0;
public void method(){
System.out.println(k);
}
}
===========================
public class Main {
public static void main(String[] args) throws IOException {
Const c = new Const();
c.method(); // 0
System.out.println(Const.WIDTH); // 200
c.k = 4;
Const.WIDTH = 300;
c.method(); // 4
System.out.println(Const.WIDTH); // 300
}
}
class 키워드는 변경될 수 없는 클래스라고 적혀있어서, 그럼 클래스 내부에 있는 멤버 변수들도 변경이 안된다는 건가? 싶어서 한 번 테스트를 해봤는데, 멤버 변수는 정상적으로 변하고 오류를 도출하지 않는 것을 볼 수 있다. 여기서 변경 될 수 없는 클래스 라는 것은 클래스의 전체적인 구조가 상속으로 인해서 변하면 안된다는 것으로 이해를 해야 한다. 말이 약간 모호한 것이 있는 것 같다.
두 번째로는 위와 같은 테스트로 인해 알겠지만, 원래 있던 전체적인 구조 자체는 변하면 안되기 때문에, 메서드에 대한 오버라이딩이 불가능하다. 어차피 final 제어자가 붙은 클래스는 자손도 가질 수 없기 때문에 사용 할 수도 없긴하다.
세 번째로 변수의 상수화 인데, 이전에 배웠던 것 처럼 final 키워드가 붙은 변수들은 수정될 수 없다.
abstract
이 파트의 마지막 부분인 abstract 이다. 추상화와 관련된 개념인데 향후에 인터페이스랑 헷갈릴 수도 있기 때문에 해당 접근자에 대해서 정확하게 개념을 이해하고 사용 할 수 있어야 한다. 우선 정의부터 살펴보자.
대상 의미
클래스 | 클래스 내에 추상 메서드가 선언되어 있음을 의미한다. |
메서드 | 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드임을 알린다. |
우선 클래스에 대한 의미를 살펴보기 전에 메서드부터 살펴보는 것이 좋을 것이다. 추상 메서드랑 완성되지 않은 기능과도 같다. 구현부가 있어야 어떤 기능에 대한 수행이 이루어질텐데 추상 메서드는 선언부만 있고 구현부는 작성되지 않은 것이다.
abstract class Test {
public abstract void test();
}
이 부분을 처음 보는 사람이면 당황할 수 있다.
클래스 내부에서 메서드를 선언했는데, 메서드에 ; 으로 마무리가 되어있어요!
라고 할 수 있는데, 저것이 추상 메서드이다. 위에서 본것처럼 난 선언만 할거야! 라고 마무리를 하는 것이다. 그럼 저 추상 클래스에 있는 추상 메서드를 구현하기 위해서는 어떻게 해야할까? 당연하게도 해당 추상 클래스를 상속받아서 그 상속받은 클래스가 오버라이딩을 통해서 구현을 해주어야 한다.
class Imple extends Test {
@Override
public void test(){
System.out.println("여기서 구현");
}
}
위 처럼 추상클래스를 상속받아서 자손이 추상클래스에 작성되어있던 추상메서드를 오버라이딩 함으로써 완성시킬 수 있다.
그럼 이제 클래스에 붙어있는 abstract 는 무엇을 의미할까? 정말 간단한데, 클래스 내부에 추상 메서드가 있으면 그건 추상 클래스이다. 내 클래스 내부에는 추상 클래스가 있어요! 하고 알려주는 것이다. 그럼 이때 주의할 부분이 있다. 우리가 얘기했듯이 추상클래스 내부에는 구현이 되어있지 않은 추상메서드가 있는데 이를 객체로 생성하면 무슨일이 발생할까? 객체지향이기 때문에 현실 세계로 가져와서 예시를 들어보자. 어떠한 설계도가 있는데, 완성되지 않은 기능이 있는채로 그 설계도를 완성시킬려고 하면 무슨 일이 발생할까? 당연히 제 기능을 못할 것이다. 따라서, 추상 클래스는 자기 스스로 객체를 생성할 수 없다.
abstract class Test {
public abstract void test();
}
public class Main {
public static void main(String[] args) {
Test t = new Test(); // 오류 발생
}
}
따라서 객체 자신의 생성을 막고 구현체를 만듦으로써 해당 클래스 자체에 대한 접근을 막아버리는 방법도 있다.
접근 제어자
접근 제어자는 아키텍쳐를 설계하는데에 있어서 굉장히 중요한 파트이다. 접근 제어자가 어디까지 동작하는 지를 알아야 클래스 혹은 멤버 변수, 메서드 등을 어떻게 처리할지에 대해 감각이 잡힐 것이다. 먼저 접근 제어자의 종류들 부터 살펴보자.
- private
- default
- protected
- public
위에 적힌 접근 제어자들은 접근 범위가 좁은 것 부터 넓은 순으로 정리를 해두었다. 본래에는 넓은 순부터 좁은순으로 할려고 했는데, 설명을 하고 서순에 맞춰서 설명을 하기에는 좁은 것부터 무엇이 차례차례로 생기는지에 대해서 설명을 하는것이 더 잘 와닿을 것 같아서 수정하였다.
private
private는 해당 클래스 내부에서만 사용이 가능하다. 즉, 다른 클래스들은 private가 선언된 것에 대해서는 접근조차 안되는 것이다. 이제 부터 하나하나 접근 권한이 가능한 것들이 생길텐데 이를 중점으로 이해하는 것이 중요할 것 같다. 해당 파트를 작성하면서 문득 의문이 들은 것이 있다. 기존에 우리는 상속을 통해서 조상 클래스에 있는 변수들을 가져다 쓸 수 있었다. 근데 private 접근 제어자의 설명을 보면 자기 자신 말고는 다른데서 사용을 할 수가 없다고 하니 그럼 해당 클래스를 상속받은 자손에서도 접근제어자가 private로 선언된 변수 및 메서드에도 접근이 불가능 한 것일까? 하고 테스트를 진행해보았다.
public class Const {
static public int WIDTH = 200;
private int k = 0;
Const(){
}
Const(int k){
this.k = k;
}
public void method(){
System.out.println(k);
}
}
=====================
public class ConstChild extends Const {
ConstChild(){
super(4);
}
@Override
public void method() {
System.out.println(k);
}
}
다음과 같이 super() 생성자를 통해서, k의 값을 초기화하고, k값에 접근하여 출력을 해보려고 시도하였으나 멤버변수 k는 접근제어자가 private이기 때문에, 접근이 불가능하다는 오류를 보았다. 이 만큼 private는 자기 자신의 클래스 내부에서만 사용할 수 있고, 그 상속 관계에 있는 어떤 자손이든 부모에도 접근이 불가능하다는 것을 알게 됐다.
default
default는 같은 패키지 내에 있는 클래스들은 접근이 가능하다. 또한 접근 제어자를 생략하면 자동으로 default로 된다는 것을 알아야 한다. 위에서 private는 절대적으로 본인의 클래스가 아닌 다른곳에서는 접근이 불가능했지만 default는 거기서 한 단계 풀어서 같은 패키지 내부에 있는 클래스들은 접근이 가능하다. 여기서 이런 궁금증이 생겼다. 같은 패키지라고 했으니까. 예전에 import를 배웠을 때랑 비슷하게 절대적으로 자기 패키지 내부에 있어야 하는 것일까? 하위 패키지들도 접근이 불가능할까? 하여 테스트를 진행해보았다.
package packageTest;
public class Default {
}
class Default2 {
int var;
}
=====================
package packageTest.childpackage
public class ChildDefault {
Default2 d = new Default2(); // 오류 발생 접근 불가
}
import에서와 동일하게 하위 패키지에는 따로 접근이 불가능한 것을 볼 수 있었다. default에서도 정확하게 자기 자신이 속한 패키지에서만 접근이 가능하다는 사실을 정확하게 알고 넘어가야 하겠다.
protected
우리는 default가 자기 자신의 패키지에서만 자신을 호출할 수 있다는 사실을 알았다. private에서 default로 변할 때, 한 구역에 접근권한이 풀린 것처럼 protected도 한 구역에 대한 접근권한이 풀리게 된다. 이제는 어떤 클래스의 자손 클래스에서는 해당 조상 클래스에 protected로 선언된 멤버 변수와 메서드에 접근을 할 수 가 있다. 여기서 이런 질문을 할 수가 있다. 그러면 클래스에 protected를 선언하면 자손 클래스만 해당 클래스에 접근을 하는 건가요? 나중에 public을 배울 때 얘기하겠지만 클래스에 대한 접근 제어자는 default와 public 밖에 지정을 할 수가 없다. protected 및 private는 클래스에는 사용할 수 없고, 멤버 변수 및 메서드에서만 사용이 가능하다.
어떤 클래스가 존재할 때 자신의 자손만 고칠 수 있는 변수가 있다거나 할 때 protected 접근 제어자를 사용한다. 좋은 예시는 아니지만 패키지를 자신의 집이라고 생각해보자. 다른 패키지는 다른 집이라고 가정해보자.
package home;
public class FamilyBankbook {
protected static int bankbook = 10000; // 10000원이 들어있음.
protected void deposit(int money){
bankbook += money;
}
protected void withdraw(int money){
bankbook -= money;
}
}
// 가족 구성원들끼리 공유하는 통장이 하나 있다고 가정하자.
// 해당 통장은 같은 집에 사는 가족 구성원들 혹은 자취를 하고 있는 가족 구성원만 가능하다.
=====================
import home.FamilyBankbook;
public class Daughter extends FamilyBankbook {
public static void main(String[] args) {
Daughter d = new Daughter();
d.deposit(10000); // 10000원을 입금하였음
System.out.println(FamilyBankbook.bankbook); // 20000이 됨
d.withdraw(5000); // 5000원을 출금하였음
System.out.println(FamilyBankbook.bankbook); // 15000이 되었음
}
}
// 딸이 대학을 가게되면서 자취를 하게 되었고, 가족들이 공유하는 통장에 접근해서 입금도하고 출금도 한다.
protected의 접근 범위는 이렇게 이해를 하면 될 것 같다. 코드를 작성하면서도 조금 헷갈리는 부분이 있긴 했다. 예를 들어서 음 딸이 통장을 포함한다? 로 구성해보는 것은 어떨까? 하고 고민했지만 포함 관계는 상속 관계가 아니기 때문에 가족 통장에 있는 변수와 메서드들에 접근할 수 없었다. 상속 관계에서만 가능하다는 사실을 놓치면 안되겠다.
public
사실 public은 설명할 수 있는게 딱히 없는게, 이제 모든 접근권한이 다 풀린 상태이다. public을 제외하고 위에 있는 3개는 접근권한이 있기 때문에 어디까지 접근을 할 수 있을지에 대해서 고민을 하고 사용해야 하지만 어떠한 접근 권한도 주기 싫을 때는 public을 사용함으로써 다른 곳 어디든 접근이 가능하도록 할 수 있다. 그렇다고 public을 남발하는 것은 좋지 않다. 참고로 아까 위에서 얘기했듯이 클래스의 접근제어자는 public과 default만 사용할 수 있다는 것을 잊지 말도록 하자.
'Language > Java' 카테고리의 다른 글
7-6 다형성 (0) | 2023.03.23 |
---|---|
7-5 캡슐화 (0) | 2023.03.23 |
7-3 패키지 (0) | 2023.03.22 |
7-2 참조변수 super, 생성자 super() (0) | 2023.03.22 |
7-1 상속과 오버라이딩 (0) | 2023.03.22 |