캡슐화는 객체의 데이터(필드)와 그 데이터를 조작하는 메서드를 하나의 단위로 묶는 것을 의미한다. 이를 통해 객체의 상태를 보호하고 외부에서 직접 접근하는 것을 제한하여 객체의 유지보수와 확장성을 향상시킬 수 있다.

// 좋지 않은 예: public 필드를 직접 사용
class PersonBadExample {
    public String name;
    public int age;

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

// 좋은 예: private 필드와 public 접근자/설정자 메서드를 사용
class PersonGoodExample {
    private String name;
    private int age;

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

    // 이름에 대한 접근자 메서드
    public String getName() {
        return name;
    }

    // 이름을 설정하는 메서드
    public void setName(String name) {
        this.name = name;
    }

    // 나이에 대한 접근자 메서드
    public int getAge() {
        return age;
    }

    // 나이를 설정하는 메서드
    public void setAge(int age) {
        if (age > 0) { // 유효성 검사 예시
            this.age = age;
        }
    }
}

// 사용 예
public class Main {
    public static void main(String[] args) {
        // 좋지 않은 예
        PersonBadExample badExample = new PersonBadExample("John Doe", 30);
        badExample.age = -5; // 유효하지 않은 나이 설정 가능
        
        // 좋은 예
        PersonGoodExample goodExample = new PersonGoodExample("Jane Doe", 30);
        goodExample.setAge(-5); // 유효성 검사를 통과하지 못해 설정되지 않음
    }
}

 

좋지 않은 예에서는 필드를 public 으로 설정해 객체의 상태를 외부에서 직접 변경할 수 있게 되어있다. 이는 잘못된 설계이다.

좋은 예 에서는 모든 필드를 private로 선언해 각 필드에 대한 접근자(getter)와 설정자(setter)메서드를 제공해주어 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻을 수 있다.

 

결론 : public 클래스는 절대 가변 필드를 노출해서는 안된다. 불변 필드라면 노출해더 덜 위험하지만 안전한것은 아니다. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을때도 있다.

"클래스와 멤버의 접근권한을 최소화하라"는 원칙은 정보 은닉(Encapsulation)과 관련된 개념으로, 코드를 작성할 때 클래스와 그 내부 멤버에 대한 접근 권한을 가능한 한 제한하는 것을 권장하는 원칙이다. 이렇게 함으로써 코드의 유지 보수성과 안정성을 높일 수 있다.

 

정보은닉의 기본 원칙 : 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 소프트웨어가 올바르게 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.

 

멤버에 부여할 수 있는 접근 수준 : 

  1. public: 이 접근 수준을 가진 멤버는 어디서든 접근할 수 있다. 즉, 해당 멤버는 외부 클래스, 패키지, 심지어 다른 모듈에서도 접근할 수 있다.
  2. protected: 이 접근 수준을 가진 멤버는 동일한 패키지 내의 클래스 및 해당 클래스를 상속한 하위 클래스에서 접근할 수 있다. 즉, protected 멤버는 패키지 외부에서는 접근할 수 없지만, 하위 클래스에서는 접근할 수 있다.
  3. default (package-private): 접근 수준을 명시하지 않는 경우, 해당 멤버는 기본(default) 접근 수준을 가진다. 이 경우, 멤버는 동일한 패키지 내의 클래스에서만 접근할 수 있다. 패키지 외부에서는 접근할 수 없다.
  4. private: 이 접근 수준을 가진 멤버는 선언된 클래스 내에서만 접근할 수 있다. 외부 클래스에서는 접근할 수 없다. 이는 정보 은닉을 위해 사용된다.

- public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.  클래스의 내부 구현 세부사항을 외부에 노출시키지 않고, 외부에서 직접적으로 접근하여 변경하지 못하도록 하는 것을 의미한다. 가능한한 클래스의 인스턴스 필드는 private으로 선언하고, 필요한 경우에만 접근자(getter)와 설정자(setter)를 제공하여 외부에서 간접적으로 접근하도록 하는 것이 바람직하다. 이를 통해 정보 은닉과 캡슐화를 유지하고, 안정성과 유연성을 높여 데이터 유효성을 보장해준다.

 

- public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다. 멀티 스레드 환경에서 해당 클래스의 인스턴스를 여 러 스레드가 동시에 접근하고 수정할 때 문제가 발생할 수 있다. 예를 들어, 한 스레드가 값을 읽는 동안 다른 스레드가 값을 변경할 수 있으며, 이로 인해 데이터 불일치가 발생할 수 있다.

 

- 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다. 

public : 모든 클래스에서 접근 할 수 있다.static :  클래스의 인스턴스화 없이 사용할 수 있다.final : 한 번 초기화되면 그 값이 변경되지 않는다. 즉, 해당 필드는 상수로 취급된다. (상수 : 고정된 값)

아래의 클래스는 public static final 배열 필드를 사용하고, 해당 필드를 반환하는 접근자 메서드를 제공한다. 보안 허점이 숨어있는 코드이다.

public class Constants {
    public static final String[] COLORS = {"Red", "Green", "Blue"};

    public static String[] getColors() {
        return COLORS;
    }
}

 

COLORS 배열을 public static final 필드로 선언하고, getColors() 메서드를 통해 외부에서 이 배열에 접근할 수 있다.

이 코드는 2가지 문제점이 있다.

  1. 불변성 보장의 부족: COLORS 배열은 public static final로 선언되어 있지만, 배열 자체는 불변이 아니다. 즉, 배열의 요소를 변경할 수 있다. 배열은 크기가 고정되고 한 번 생성되면 수정할 수 없는 자료 구조이다. 이러한 배열을 public static final 필드로 제공하면 해당 배열은 수정할 수 없는 상수 배열로 간주된다. 그러나 배열이 참조 타입이기 때문에 해당 배열을 참조하는 다른 변수들이 배열 내의 요소를 변경할 수 있어서 불변성을 보장하지 않는다.
  2. 캡슐화 원칙 위배: 외부에서 COLORS 배열에 직접 접근할 수 있으므로 캡슐화 원칙을 위배한다. 캡슐화는 클래스의 내부 상태를 외부에서 직접 접근할 수 없도록 보호하는 것을 의미한다. 배열의 참조를 변경하는 것은 불가능하지만, 배열 내의 요소를 변경하는 것은 가능하다. 예를 들어, Constants.COLORS[0] = "Yellow"; 같이 배열의 요소를 변경할 수 있다.

아래처럼 코드를 수정하면 해결할 수 있다.

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Constants {
    // private static final로 선언된 배열 COLORS
    private static final String[] COLORS = {"Red", "Green", "Blue"};

    // getColors 메서드: COLORS 배열을 변경할 수 없는 리스트 형태로 반환
    public static List<String> getColors() {
        // Arrays.asList 메서드를 사용하여 COLORS 배열을 리스트로 변환
        // 이 메서드는 고정 크기의 리스트를 반환하므로 리스트의 크기는 변경될 수 없음
        // unmodifiableList 메서드를 사용하여 리스트를 변경할 수 없는 리스트로 래핑
        return Collections.unmodifiableList(Arrays.asList(COLORS));
    }
}

public 배열을 private 으로 만들고 public 불변 리스트를 추가해주면 된다.

 

결론 : 프로그램 요소의 접근성은 가능한 한 최소한으로 하자. 꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개 되는 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하자.

https://www.acmicpc.net/problem/1068

 

1068번: 트리

첫째 줄에 트리의 노드의 개수 N이 주어진다. N은 50보다 작거나 같은 자연수이다. 둘째 줄에는 0번 노드부터 N-1번 노드까지, 각 노드의 부모가 주어진다. 만약 부모가 없다면 (루트) -1이 주어진다

www.acmicpc.net

 

 

[정답 코드]

import java.io.*;
import java.util.*;

public class Main {
    static int root;
    static Node[] tree;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        int N = Integer.parseInt(st.nextToken());
        tree = new Node[N];

        st = new StringTokenizer(br.readLine(), " ");
        for (int i = 0; i < N; i++) {
            int parent = Integer.parseInt(st.nextToken());
            tree[i] = new Node(parent); // 노드의 번호는 i, i번 노드의 부모는 parent 인 Node를 생성
            if (parent == -1) { // parent 번호가 -1 이면 해당 노드는 루트 노드
                root = i;
            }
        }

        // 루트 노드가 아닌 경우, 해당 노드의 부모 노드를 찾아 자식노드에 자신을 추가
        for (int i = 0; i < N; i++) {
            if (root != i) { // 루트노드가 아닐경우
                int parent = tree[i].parent; // i 번 노드의 부모노드를 꺼내서
                tree[parent].addChild(i); // 부모노드의 자식에 i번 노드를 추가해줌
            }
        }

        int removeNode = Integer.parseInt(br.readLine()); // 삭제 할 노드 번호
        if (removeNode == root) { // 루트 노드를 지울경우 전부 삭제되므로 0 출력
            System.out.println(0);
        } else {
            int parent = tree[removeNode].parent; // 삭제하려는 노드의 부모 노드의 값을 꺼냄
            tree[parent].removeChild(removeNode); // 그 부모노드의 자식노드들 중 삭제하려는 노드번호를 삭제
            int leafSize = getLeafSize(root); // 리프노드의 수를 카운팅
            System.out.println(leafSize);
        }
    }

    static int getLeafSize(int index) {
        if (tree[index].isLeaf()) { // 해당 노드의 자식노드가 없다면 true 를 반환해서
            return 1; // 1 반환
        }
        int sum = 0;
        for (int i = 0; i < tree[index].getChildrenSize(); i++) {
            sum += getLeafSize(tree[index].children.get(i)); // 반복문을 재귀적으로 돌며 자식노드가 없는 노드들을 탐색
        }
        return sum;
    }
}

// 노드 클래스
class Node {
    int parent; // 해당 노드의 부모 노드
    List<Integer> children; // 자식노드들

    public Node(int parent) {
        this.parent = parent;
        this.children = new ArrayList<>(); // 자식노드들은 ArrayList 로 생성
    }

    public void addChild(int child) { // 노드의 자식들을 ArrayList.add 로 삽입
        this.children.add(child);
    }

    public void removeChild(int child) { // 노드의 자식들을 ArrayList.remove 로 삭제
        this.children.remove(Integer.valueOf(child));
    }

    public boolean isLeaf() { // 노드의 자식이 없다면 true 반환
        return this.children.isEmpty();
    }

    public int getChildrenSize() { // 노드의 자식들의 개수를 ArrayList.size() 메서드를 활용해 출력
        return this.children.size();
    }
}

 

[설명]

저번에 풀었던 노드의 심화버전이다. 그래서 저번에 풀었던 코드를 조금만 수정해 풀려고 했으나 실패해 포기해 다른분들의 코드를 찾아보며 풀어버렸다.

예제 입력 1번을 예시로 설명을 해보자면

 

1. Node클래스 배열을 선언

  • 노드배열의 인덱스가 해당 노드의 밸류
    노드 클래스의 필드는 parent(부모노드번호), childrent(자식노드들을 ArrayList로 가변적으로 선언)
  • tree[0] = new Node(-1)
    tree[1] = new Node(0)
    tree[2] = new Node(0)
    tree[3] = new Node(1)
    tree[4] = new Node(1)
    root = 0

2. 각 클래스들의 부모를 찾아 자기자신을 그 부모노드의 자식으로 추가함

ArrayList.add 를 사용해 추가

  • i=0
    int parent = tree[0].parent = -1 // parent가  root 일 경우 if문 false

    i = 1
    int parent = tree[1].parent = 0
    tree[0].addChild(1);

    i = 2
    int parent = tree[2].parent = 0
    tree[0].addChild(2);

    i = 3
    int parent = tree[3].parent = 1
    tree[1].addChild(3);

    i = 4
    int parent = tree[4].parent = 1
    tree[1].addChild(4);

3. 삭제할 노드를 입력받아 해당 노드를 삭제함

  • 삭제할 노드를 입력받고 해당 노드의 부모노드를 탐색 -> 삭제하려는 부모노드의 자식들중 삭제하려는 노드와 같은 자식을 ArrayList.remove() 메서드로 삭제
  • removeNode = 2
    int parent = tree[2].parent = 0
    tree[0].removeChild(2)

4. 리프 노드를 카운팅

  • 루트노드부터 시작해 모든 자식노드들을 방문하며 어떤 노드의 Children.isEmpty가 true일경우 sum 을 1씩 증가시킴
  • 이 로직을 재귀적으로 반복해가며 모든 노드들을 방문함

 

'코딩테스트' 카테고리의 다른 글

백준 10819번: 차이를 최대로[JAVA]  (0) 2024.03.21
백준 1260번: DFS와 BFS[JAVA]  (0) 2024.03.20
백준 1991번: 트리 순회[JAVA]  (1) 2024.03.15
백준 16234번: 인구 이동[JAVA]  (1) 2024.03.13
백준 2493번: 탑[JAVA]  (0) 2024.03.12

https://www.acmicpc.net/problem/1991

 

1991번: 트리 순회

첫째 줄에는 이진 트리의 노드의 개수 N(1 ≤ N ≤ 26)이 주어진다. 둘째 줄부터 N개의 줄에 걸쳐 각 노드와 그의 왼쪽 자식 노드, 오른쪽 자식 노드가 주어진다. 노드의 이름은 A부터 차례대로 알파

www.acmicpc.net

 

 

 

[정답 코드]

import java.io.*;
import java.util.*;

public class Main {
    static Node head = new Node('A', null, null); // 트리는 A부터 시작
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        int N = Integer.parseInt(st.nextToken());

        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine(), " ");
            char root = st.nextToken().charAt(0);
            char left = st.nextToken().charAt(0);
            char right = st.nextToken().charAt(0);

            insertNode(head, root, left, right);
        }

        preOrder(head);
        System.out.println();
        inOrder(head);
        System.out.println();
        postOrder(head);
        System.out.println();
    }


    static class Node {
        char value;
        Node left;
        Node right;

        Node(char value, Node left, Node right) {
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

    public static void insertNode(Node tmp, char root, char left, char right) {
        if (tmp.value == root) {
            tmp.left = (left == '.' ? null : new Node(left, null, null));
            tmp.right = (right == '.' ? null : new Node(right, null, null));
        } else {
            if (tmp.left != null) {
                insertNode(tmp.left, root, left, right);
            }
            if (tmp.right != null) {
                insertNode(tmp.right, root, left, right);
            }
        }
    }

    private static void preOrder(Node node) {
        if (node == null) { // 더 이상 값이 없을 때 까지
            return;
        }
        System.out.print(node.value); // 출력
        preOrder(node.left); // 왼쪽 노드 탐색
        preOrder(node.right); // 오른쪽 노드 탐색
    }

    private static void inOrder(Node node) {
        if (node == null) { // 더 이상 값이 없을 때 까지
            return;
        }
        inOrder(node.left); // 왼쪽 노드 탐색
        System.out.print(node.value); // 출력
        inOrder(node.right); // 오른쪽 노드 탐색
    }

    private static void postOrder(Node node) {
        if (node == null) { // 더 이상 값이 없을 때 까지
            return;
        }
        postOrder(node.left); // 왼쪽 노드 탐색
        postOrder(node.right); // 오른쪽 노드 탐색
        System.out.print(node.value); // 출력
    }
}

 

[설명]

트리를 순회하는 문제이다. 트리 탐색의 아주 기초문제이지만 너무 오랜만에 풀어서 도저히 풀 수가 없어 다른 글을 참고했다.

먼저 해결 방법은 다음과 같다.

 

1. Node 클래스를만든다.

  • Node 클래스는char value, Node left, Node right 구조를 가진 클래스이다.

2. Node 클래스에 insertNode메서드를 활용해 값을 입력한다.

  • insertNode는 (Node tmp, char root, char left, char right) 매개변수를 가지는데
    맨 처음 선언한 Node head 를 기준으로 null 이 아닌 노드를 계속 탐색한다.
    입력한 char root 값이 어떠한 Node의 value라면 그 노드의 왼쪽 노드와 오른쪽 노드에 
    값을 집어 넣는다.

3. 전위 순회 ,중위 순회 ,후위 순회 메서드를 각 각 사용해 출력한다.

  • 전위 순회 : 루트 -> 왼쪽  노드 -> 오른쪽 노드 순서
    중위 순회 : 왼쪽 노드 -> 루트 -> 오른쪽 노드 순서
    후위 순회 : 왼쪽 노드  -> 오른쪽 노드 -> 루트 순서

디버깅모드로 차근차근 이해해보면 금방 이해할 수 있다.

'코딩테스트' 카테고리의 다른 글

백준 1260번: DFS와 BFS[JAVA]  (0) 2024.03.20
백준 1068번: 트리[JAVA]  (1) 2024.03.15
백준 16234번: 인구 이동[JAVA]  (1) 2024.03.13
백준 2493번: 탑[JAVA]  (0) 2024.03.12
백준 7682번: 틱택토[JAVA]  (0) 2024.03.11

Comparable 인터페이스는 객체들을 순서대로 정렬할 수 있도록 한다. 이 인터페이스를 구현하는 클래스는 compareTo 메서드를 오버라이드 해야하며, 이 메서드는 객체 자신과 다른 객체를비교하여 정렬 순서를 결정하는데사용된다.

 

compareTo 메서드의 일반 규약

 


1. 호출 결과의 부호는 첫 번째 객체가 두 번째 객체보다 작은지, 같은지, 큰지를 나타낸다.

  • compareTo가 반환하는 값이 0보다 작으면, 첫 번째 객체가 두 번째 객체보다 작다는 것을 의미합니다.
  • compareTo가 반환하는 값이 0이면, 두 객체는 동일하다는 것을 의미합니다.
  • compareTo가 반환하는 값이 0보다 크면, 첫 번째 객체가 두 번째 객체보다 크다는 것을 의미합니다.


2. 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.

  • x<y 면 y>x 다
  • x==y 면 y==x다
  • x>y 면 y<x다

3. 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야한다.

  • x>y이고 y>z 면 x>z다

4. 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.

5. 두 객체가 compareTo로 0을 반환할 경우 equals 메서드에서 true를  반환해야 한다.

  • 그러나 equals가 true를 반환할 경우에 compareTo가 0을 반환할 필요는 없다.( compareTo는 equals보다 더 세밀한 비교를 제공할 수 있음)

compareTo 메서드를 재정의시 관계 연산자 '<'와 '>'를 사용하면 안된다.

이따금 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo나 comapre 메서드를 마주치는데 이 방식을 사용해서는 안된다. 정수 오버플로나 부동소수점 계산 방식에따른 오류가 발생할 수 있기 때문이다.

해시코드 값의 차를 기준으로 하는 비교자 - 추이성에 위배되므로 권장되지 않음!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Obejct o1, Object o2) {
    	return o1.hashCode() - o2.hashCode();
    }
};

권장되는 정적 compare 메서드를 활용한 비교자 예시
static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Obejct o1, Object o2) {
    	return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = 
		Comparator.comparingInt(o -> o.hashCode());

 

 

 

결론 : 순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 피교할 때는 박싱된 기본 타입 클래스가 제공하는 '정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

 

+ Recent posts