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

 

7682번: 틱택토

입력은 여러 개의 테스트 케이스로 이루어져 있다. 각 줄은 9개의 문자를 포함하며, 'X', 'O', '.' 중 하나이다. '.'은 빈칸을 의미하며, 9개의 문자는 게임판에서 제일 윗 줄 왼쪽부터의 순서이다. 입

www.acmicpc.net

 

[정답 코드]

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

public class Main {
    static char[][] board;
    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;
        String input = "";
        board = new char[3][3];
        while (true) {
            st = new StringTokenizer(br.readLine(), " ");
            input = st.nextToken();
            if (Objects.equals(input, "end")) {
                break;
            }
            int index = 0;
            int xCount = 0;
            int oCount = 0;
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    board[i][j] = input.charAt(index);
                    if (board[i][j] == 'O') {
                        oCount++;
                    } else if (board[i][j] == 'X') {
                        xCount++;
                    }
                    index++;
                }
            }
            //게임판이 꽉 채워졌을 때
            //X가 먼저 말을 놓았음으로 O보다 1개 무조건 많아야 한다.
            if (xCount + oCount == 9 && xCount - oCount == 1) {
                //한 명이 빙고를 완성하면 게임이 끝나기 때문에
                //둘 다 빙고가 성립될 수 없다.
                if (isValid('X') && isValid('O')) {
                    bw.write("invalid\n");
                } else if (isValid('O')) {//말이 꽉 채워진 경우에는 O가 이길 수 없다
                    bw.write("invalid\n");
                } else { // X 가 이긴경우
                    bw.write("valid\n");
                }
            } else { // 게임판이 꽉 채워지지 않았을 경우
                //한 명이 빙고를 완성하면 게임이 끝나기 때문에
                //둘 다 빙고가 성립될 수 없다.
                if (isValid('X') && isValid('O')) {
                    bw.write("invalid\n");
                } else if (isValid('X') && xCount - oCount == 1) { //X가 이겼을 땐, X가 선공이어서 무조건 O보다 하나 많아야 한다
                    bw.write("valid\n");
                } else if (isValid('O') && xCount == oCount) { //O가 이겼을 땐, O가 후공이여서 X와 O의 개수가 같아야 한다
                    bw.write("valid\n");
                } else { // 아직 게임판이 덜채워졌는데 게임이 끝날 수는 없다
                    bw.write("invalid\n");
                }
            }
        }
        bw.flush();
        bw.close();
        br.close();
    }

    public static boolean isValid(char c) {
        //가로가 성립할 때
        for (int i = 0; i < 3; i++) {
            int count = 0;
            for (int j = 0; j < 3; j++) {
                if (board[i][j] == c) {
                    count++;
                }
            }
            if (count == 3) {
                return true;
            }
        }

        //세로가 성립할 때
        for (int i = 0; i < 3; i++) {
            int count = 0;
            for (int j = 0; j < 3; j++) {
                if (board[j][i] == c) {
                    count++;
                }
            }
            if (count == 3) {
                return true;
            }
        }

        // 대각선이 성립할 때
        if (board[0][0] == c && board[1][1] == c && board[2][2] == c) {
            return true;
        }
        if (board[0][2] == c && board[1][1] == c && board[2][0] == c) {
            return true;
        }

        return false;
    }
}

 

[설명]

단순 구현 문제이다. 크게 봤을 때는 게임판에 말이 가득 찼을 경우와 가득차지 않았을 경우로 나눌수 있다.

 

  1. 말이 가득찼을 경우(X가 무조건 O보다 한개 많다)
    • X와 O둘다 동시에 이길 수는 없다
    • O가 이길수는 없다
    • X가 이긴다
    • 둘다 못 이긴다
  2. 말이 가득차지 않았을 경우
    • X와 O둘다 동시에 이길 수는 없다
    • X가 이겼을 땐 X가 선공이라 X가 O보다 1개 많아야 한다
    • O가 이겼을 땐 O가 후공이라 X와 O는 같아야 한다
    • 아직 게임판이 덜 채워졌는데 게임이 끝날 수는 없다

 

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

백준 16234번: 인구 이동[JAVA]  (1) 2024.03.13
백준 2493번: 탑[JAVA]  (0) 2024.03.12
백준 9372번: 상근이의 여행[JAVA]  (0) 2024.03.09
백준 2668번: 숫자고르기[JAVA]  (0) 2024.03.07
백준 5972번: 택배 배송[JAVA]  (0) 2024.03.07

try-finally 사용 예시 (비효율적)

import java.io.*;

public class TryFinallyExample {
    public static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(path));
            return br.readLine();
        } finally {
            if (br != null) {
                br.close(); // 자원을 명시적으로 해제해야 함
            }
        }
    }
}

이 예시에서는 finally 블록을 사용해 파일 리더를 명시적으로 닫아야한다. 번거로울 뿐만 아니라 close 메서드에서 예외가 발생하면 원래의 예외가 가려질 수도 있어서 문제가 된다.

 

try-with-resources 사용 예시 (권장)

import java.io.*;

public class TryWithResourcesExample {
    public static String firstLineOfFile(String path) throws IOException {
        // try-with-resources 구문 사용
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine(); // 자동으로 리소스를 해제해 줍니다.
        }
    }
}

이 예시에서는 BufferedReader의 인스턴스를 자동으로 관리한다. 구문이 끝나면 자동으로 BufferedReader가 닫히므로 명시적으로 자원을 해제하는 코드를 작성할 필요가 없다. 또한 try 블록 내에서 예외가 발생하더라도 자원은 안전하게 해제된다. close메서드에서 예외가 발생하는 경우에도 원래 예외를 숨기지 않고 보조 예외(suppressed exception)로 처리한다.

 

결론 : 꼭 회수해야하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자.

반드시 사용하자. 예외는 없다.

코드를 더 간결하고 안정하게 만들고 자원 해제 과정에서 발생할 수 있는 잠재적인 문제를 방지한다.

혹시나 코드가 더 길어지더라도 try-with-resources를 사용하자.

자바는 두 가지 객체 소멸자를 제공한다.

finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

사용 예시 : 

public class ProblematicResourceHolder {
    private SomeResource resource;

    public ProblematicResourceHolder(String resourceName) {
        this.resource = new SomeResource(resourceName);
    }

    // Finalizer를 사용한 자원 해제 시도 - 사용을 피해야 함!
    @Override
    protected void finalize() throws Throwable {
        try {
            if (resource != null) {
                resource.release(); // 자원 해제 시도
            }
        } finally {
            super.finalize();
        }
    }
}

finalizer를 사용하는 간단한 예시 이다.

안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 finalizer 혹은 cleaner를 사용하는게 좋다.

하지만 그냥 둘다 사용 자체를 하지않는게 좋은것 같다.

 

 

 

결론 : finalizer, cleaner 둘다 사용하지말자

자바의 가비지 컬렉션(garbage collection) 메커니즘이 자동으로 메모리 관리를 해주더라도, 더 이상 사용하지 않는 객체에 대한 참조를 계속 유지하는 것은 메모리 누수(memory leak)를 일으킬 수 있다. 특히, 컬렉션 객체 같은 경우는 개발자가 명시적으로 객체 참조를 해제하지 않으면, 의도치 않게 메모리 누수가 발생할 수 있다.

 

스택 구현에서의 메모리 누수 방지 예시 : 

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

    /**
     * 스택이 커질 필요가 있을 때 크기를 늘려준다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

여기서 핵심은 pop 메서드에서 스택에서 제거된 객체(Object result = elements[--size])에 대한 참조를 null로 설정하여 명시적으로 해제하는 부분이다. 이제 가비지 컬렉터가 필요할 때 이전에 스택에 저장되었던 객체를 회수할 수 있다. 만약

pop 메서드에서 객체 참조를 null로 설정하지 않는다면 스택이 그 객체에 대한 참조를 계속 유지하게 된다. 더 이상 사용되지 않는 객체임에도 불구하고 가비지 컬렉터가 회수하지않아 장시간 동안 어플이 실행될 경우 메모리 누수가 일어난다. 

 

결론 : 다 쓴 객체 참조를 해제하는 것은 메모리 관리와 애플리케이션 성능에 있어 중요한 사항이다.

똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 

 

예시 : 문자열 인스턴스의 불필요한 생성 피하기

비효율적인 예
public String repeat(String str, int count) {
    String result = "";
    for (int i = 0; i < count; i++) {
        result += str; // 매 반복마다 새로운 String 객체 생성
    }
    return result;
}

효율적인 예
public String repeat(String str, int count) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < count; i++) {
        result.append(str); // 동일한 StringBuilder 객체를 재사용
    }
    return result.toString();
}

자바에서 String 객체는 불변이다. 불변이란 한 번 생성된 String 객체의 내용이 변경될 수 없다는뜻이다.  비 효율적인 예에서 문자열을 반복적으로 연결할 때마다 새로운 String 객체가 생성되어 메모리 사용량이 증가하고 성능이 저하된다.

 

예시 : 불변 객체의 재사용

비효율적인 예
Boolean trueValue = new Boolean(true); // 불필요한 Boolean 객체 생성
Boolean falseValue = new Boolean(false);

효율적인 예
Boolean trueValue = Boolean.valueOf(true); // Boolean 객체 재사용
Boolean falseValue = Boolean.valueOf(false);

불변 객체는 내부 상태가 변경되지 않으므로 같은 값을 가지는 인스턴스를 여러번 생성할 필요가 없다. 

예를 들어, Boolean.valueOf(boolean) 메소드는 Boolean.TRUE 또는 Boolean.FALSE를 반환하여, 같은 Boolean 객체를 재사용할 수 있게 한다.

 

결론 : 기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라.

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식을 사용하는 경우가 자주 있는데 이는 적합하지 않다.

인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식인 의존 객채 주입 패턴이 좋다.

 

예시 : 

// 주문 처리를 담당하는 클래스
public class OrderProcessor {
    // 결제 서비스에 대한 의존성
    private final PaymentService paymentService;
    // 재고 서비스에 대한 의존성
    private final InventoryService inventoryService;

    // 생성자를 통한 의존 객체 주입
    // OrderProcessor는 PaymentService와 InventoryService의 구체적인 구현에 의존하지 않습니다.
    // 대신, 이 서비스들은 외부(예: 애플리케이션의 설정 부분)에서 주입됩니다.
    public OrderProcessor(PaymentService paymentService, InventoryService inventoryService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }

    // 주문 처리 메서드
    public void processOrder(Order order) {
        // 결제 처리 시도
        if (paymentService.processPayment(order.getTotalPrice())) {
            // 결제 성공 시, 재고 업데이트
            inventoryService.updateInventory(order.getItems());
            System.out.println("Order processed successfully.");
        } else {
            // 결제 실패 시
            System.out.println("Payment failed.");
        }
    }
}

// 결제 처리를 위한 인터페이스
public interface PaymentService {
    boolean processPayment(double amount); // 결제 처리 메서드
}

// 재고 관리를 위한 인터페이스
public interface InventoryService {
    void updateInventory(List<Item> items); // 재고 업데이트 메서드
}

// 애플리케이션 실행 클래스
public class App {
    public static void main(String[] args) {
        // 결제 서비스와 재고 서비스의 구현체를 생성
        PaymentService paymentService = new CreditCardPaymentService(); // 실제 결제 서비스 구현체
        InventoryService inventoryService = new WarehouseInventoryService(); // 실제 재고 관리 서비스 구현체

        // OrderProcessor 인스턴스 생성 시, 의존 객체를 주입
        OrderProcessor orderProcessor = new OrderProcessor(paymentService, inventoryService);
        
        // 주문 처리
        orderProcessor.processOrder(new Order(/* 주문 정보 */)); 
    }
}

 

예시의 OrderProcessor 클래스를보면 OrderProecessor는 paymentService 와 inventoryService 인터페이스에 의존한다. 하지만 구체적인 구현에는 의존하지 않는다. 앱 실행 클래스를 보면 이 두 서비스의 구체적인 구현은 생성자를 통해 주입된다. 

 

결론 : 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 필요한 자원(혹은 그 자원을 만들어주는 팩토리를) 생성자에(혹은 정적 팩토리나 빌더에) 넘겨주자. = 의존 객체 주입 기법을 사용해 클래스의 유연성, 재사용성, 테스트 용이성을 개선해주자.

"인스턴스화를 막으려거든 private 생성자를 사용해라"는 조언은 유틸리타 클래스나 도우미 클래스와 같이 상태를 가지지 않고 단순 메서드만을 제공하는 클래스에서 주로 사용된다.

 

예시 : 

public class UtilityClass {
    // private 생성자를 추가하여 인스턴스화를 방지함
    private UtilityClass() {
        throw new AssertionError("UtilityClass should not be instantiated");
    }

    // 유틸리티 메서드 예시
    public static void utilityMethod() {
        // 메서드 구현
    }
}

 

예시에 나온 UtilityClass 같이 오직 메서드만을 제공할 경우 private 생성자를 추가해, 외부에서 인스턴스 생성을 막을 수 있다. 생성자 내부에서 AssertionError를 던지는 것은 내부에서 인스턴스를 생성하려는 시도를 방지하기 위함이다. 

이렇게 하면 클래스 설계 의도가 명확해지고, 불필요한 인스턴스 생성으로 인한 리소스 낭비를 방지할 수 있다.

모든 생성자를 private 으로 선언하지않았다면 Java에서 자동으로 기본생성자 public 을 추가해 어디서나 인스턴스를 생성할 수 있게된다.

 

결론 : private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

싱글턴(Singleton) : 인스턴스를 오직 하나만 생성할 수 있는 클래스

싱글턴 패턴 : 특정 클래스의 인스턴스가 오직 하나만 생성되어서 전역적으로 접근 가능하도록 보장하는 패턴이다.

이 패턴은 전역 상태를 관리하거나, 자원 관리, 로깅, 드라이버 객체 등 한 번의 인스턴스 생성으로 충분한 경우에 주로 사용된다. 

싱글턴을 만드는 방식은 크게 2가지가 있다.

1. Private 생성자를 이용한 방식

2. Enum(열거형)을 이용한 방식

 

열거 타입을 이용한 방식의 예시 : 

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // 싱글턴 인스턴스가 할 작업 구현
    }
}

 

더 자세한 예시 : 

public enum DatabaseConnection {
    INSTANCE; // Enum 싱글턴의 인스턴스 선언. JVM은 이 인스턴스를 유일하게 관리합니다.

    private Connection connection; // 데이터베이스 연결 객체를 저장할 변수.

    // Enum의 생성자. Enum 인스턴스가 생성될 때 (즉, JVM이 시작할 때) 한 번만 호출됩니다.
    DatabaseConnection() {
        // 여기에서 데이터베이스 연결을 설정합니다. 실제 환경에서는 이 부분에
        // 데이터베이스 연결을 위한 JDBC URL, 사용자 이름 및 비밀번호 등이 포함됩니다.
        try {
            // DriverManager.getConnection을 사용하여 데이터베이스 연결을 초기화합니다.
            // 이 예제에서는 연결 정보를 가정하고 있으며, 실제 정보로 대체해야 합니다.
            this.connection = DriverManager.getConnection("jdbc:yourDatabaseUrl", "username", "password");
        } catch (SQLException e) {
            // 데이터베이스 연결 설정 중 예외가 발생한 경우, 예외를 던집니다.
            // 이를 통해 초기화 실패를 알리고, 애플리케이션 시작을 중단할 수 있습니다.
            throw new RuntimeException("Database connection setup failed", e);
        }
    }

    // 데이터베이스 연결 객체를 반환하는 메서드.
    // 이 메서드를 통해 어디서든 데이터베이스 연결을 사용할 수 있습니다.
    public Connection getConnection() {
        return this.connection;
    }

    // 데이터베이스 연결을 안전하게 닫는 메서드.
    // 애플리케이션 종료 시 또는 더 이상 데이터베이스 연결이 필요 없을 때 호출합니다.
    public void closeConnection() {
        if (this.connection != null) {
            try {
                this.connection.close();
            } catch (SQLException e) {
                // 연결 닫기 시도 중 예외 처리.
                // 예외 처리 로직에 따라 로깅 또는 다른 조치를 취할 수 있습니다.
            }
        }
    }
}

 

결론 : 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

+ Recent posts