삼성처럼 C, C++, Java 언어만 허용되는 코딩테스트였다. 그래서 그런지 네이버나 라인에 비해 알고리즘의 난이도는 쉬운 편이었다. 자바를 공부하기 시작하고서 처음으로 응시하는 언어제한 코딩테스트였는데 결과는 나쁘지 않아서 다행이다. 확실히 네이버나 라인에 비해 input 크기가 큰 문제가 출제되는 것으로 보아 시간에 대한 생각을 한 번쯤 해야하겠다. 결과는 2솔.

1번은 배열을 다루는 문제인데 연산 종류에 따라 처리 로직을 분기해야 한다. 그런데 연산 요청이 10만 건으로 아무런 생각없이 array 선언하여 값 기반으로 탐색하는 로직을 작성했다가, 자칫하다간 50000*50000번의 내부 탐색을 진행할 수도 있음을 알아챘다. merge 처리 시 단순 ArrayList 객체를 선언하여 해결하면 테스트 케이스는 통과할지언정 히든 테스트케이스로 10만 건의 요청이 입력되었을 경우 TLE를 출력할 가능성이 높아보인다. 흔한 백준 등 PS 사이트에서 쓰는 트릭을 사용해야 하는데, 유효 카운트를 체크하면서 번호 값 수정은 최대값인 10만 길이의 배열을 선언하여 좌표 기준으로 접근해야 한다.

2번은 HashMap 자료구조를 사용하면 비교적 수월하게 풀린다. input 범위도 그리 크지 않아 값에 따라 1씩 증가시킨 뒤에 HashMap 객체에 담겨진 모든 key에 대해 조회하여 배열에 넣은 후 정렬, 주어진 개수만큼 더해서 출력하면 된다.

3번은 순열문제로 c++이면 stl 내장 라이브러리로 해결하겠으나 하필 Java는 순열을 제공하는 내장 라이브러리 따위는 없기에 별개의 재귀 메소드를 선언해야 한다. 다행인 점은 표본의 개수가 8이라 8!을 하더라도 최대 4만 건에 대해 검사를 진행하고 검사해야할 경우의 수도 10개 가량에 역순까지 고려하면 20개, 매번 표본의 길이만큼 검사한다 생각하면 140 * 4만 정도로 500만 이하의 연산으로 정답을 도출해낼 수 있다. 재귀함수에서 매개변수와 반환 값이 어떻게 동작하는지 아직은 경험이 부족해서 30분 간 개삽질을 하다가 그냥 클래스 변수를 선언해서 조건에 맞을 때 값을 갱신하게끔 하여 풀었다.

후기를 쓰자면 구글은 신이고 나는 등신이었다. 역시 자바는 강력한 언어이고 좋은 언어임은 알지만 PS한정으로는 진짜.. 거지같은 언어라 생각한다. 파이썬으로 풀다가 자바로 작성하려니 무수히 많은 문법 에러들과의 악수를.... 풀이 결과는 2솔로 보인다. 1번의 문제요건을 제대로 읽지 않고 구현에 급급했던 내 불찰이다. 자료구조 예제 코드나 빈출 알고리즘 예제를 미리 띄워두고 이리저리 이동하면서 풀었는데 역시 자주 숙달해둬야 심력소모가 덜하지 않을까. 난이도를 평가하자면 3문제 모두 실버 5~ 실버 1이라 생각하는데 언어 제한이 걸려서 비교적 쉬운 문제로 출제하지 않았을까 싶다. NHN이 만만한 기업은 아닐테니 CS와 기술면접에서 변별력을 확보하려는 취지로 보인다.

결과 : 불합격했습니다. 역시 자바로 PS를 풀어본 적이 한번도 없던 게 패착으로 보입니다.

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

문제 제기

오버로딩과 오버라이딩 개념은 굉장히 자주 쓰이고 중요하다고 여겨진다. 그러나 어떤 경우에 제약이 걸리고 예외에 대한 내용은 조금 더 찾아봐야 알 수 있다. 배우는 김에 정리해보려 한다.

오버로딩(overloading)

클래스 내에 이미 동일한 이름을 갖는 메소드가 있더라도 매개변수, 리턴 타입을 다르게 하여 새롭게 메소드를 정의하는 방식이다. 컴파일타임 다형성(compiletime polymorphism)/정적 바인딩(Static Binding)에 해당하며 이미 JVM에 로드될 적에는 컴파일러에 의해 메소드 시그니처 기준으로 전부 구분되므로 JVM에서 별도로 메소드 식별 작업을 수행하지 않는다. 컴파일 수행하여 ".class" 파일 생성 시 overloading된 메소드는 메소드 이름이 동일하지만 메소드 이름 + 매개변수를 바탕으로 메소드 시그니처를 생성하여 저장한다. 따라서, 클래스 메타 정보에서 메소드들이 구분된 상태로 저장되기 때문에 컴파일이 완료되면 사실상 별개의 메소드로 간주한다.

오버로딩을 수행하면 다른 매개변수를 갖지만 동작이 비슷한 메소드의 이름을 절약할 수 있고 매개변수 별로 조건문이 아니라 메소드를 완전히 구분하는 이점을 갖고 있다.

오버로딩의 조건

  • 메소드 이름이 동일해야 한다.
  • static 메소드도 오버로딩이 가능하다.
  • 매개변수는 반드시 변화가 있어야 하며 리턴 타입은 자유롭다.
  • 매개변수는 같고 리턴 타입이 다르면 오버로딩이 성립하지 않는다.
    • 메소드 시그니처는 메소드의 이름과 매개변수로 작성되기에 리턴타입만 다르면 시그니처가 같아서 컴파일 오류가 발생한다.
  • 오버로딩된 메소드는 매개변수에 의해서만 구분이 된다.

오버로딩 예

class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }

    public void print(int val) {
        System.out.println("value is = " + val);
    }

    public void print(String val) {
        System.out.println("value is = " + val);
    }

    public void print(int val, String strData) {
        System.out.println("value is = " + val + ", " + strData);
    }

    public void print(String strData, int val) {
        System.out.println("value is = " + strData + ", " + val);
    }

}

메소드 시그니처는 메소드 이름 + 매개변수(매개변수 순서도 다르면 다르게 인식)이므로 위와 같이 다른 메소드 시그니처를 갖게끔만 보장하면 overloading이 가능하다.

오버라이딩(overriding)

부모 클래스를 상속받은 자식 클래스에서 부모 클래스의 메소드와 메소드 이름, 매개변수의 개수와 순서, 리턴 타입이 동일하지만 메소드의 동작을 다르게 선언하는 방식이다. 런타임 다형성(runtime polymorphism)/동적 바인딩(Dynamic Binding)에 해당하며 컴파일 시 모두 동일한 시그니처를 가지고 있다. 그러므로 컴파일러는 메소드를 호출한 객체가 오버라이딩한 메소드를 가지고 있는지 알 수 없다. 따라서 메소드 호출 시 변수에 할당된 객체가 오버라이딩 메소드를 정의하지 않았다면 부모 클래스의 메소드를 호출하고 정의했다면 할당된 객체 클래스의 오버라이딩 메소드를 호출하는 등 JVM에게 별도의 동작을 요구한다.

오버라이딩을 수행하면 다른 매개변수를 갖지만 동작이 비슷한 메소드의 이름을 절약할 수 있고 매개변수 별로 조건문이 아니라 메소드를 완전히 구분하는 이점을 갖고 있다.

오버라이딩의 조건

  • 메소드 이름이 동일해야 한다.
  • static 메소드는 오버라이딩이 불가능하다.
  • 매개변수의 개수, 타입, 순서가 같아야 한다.
    • 리턴 타입은 원칙적으로 동일해야 하나 Java 5 이후로는 예외가 존재한다.
      • 리턴 타입이 void인 경우 상속받는 클래스의 메소드 리턴 타입도 void여야 한다.
      • 리턴 타입이 기본 자료형일 경우 상속받는 클래스의 메소드 리턴 타입은 동일해야 한다.
      • 리턴 타입이 참조 자료형일 경우 상속받는 클래스의 메소드 리턴 타입은 동일하거나 부모 클래스의 메소트 리턴 타입의 하위 클래스여야 한다.
    • 접근 제어자는 자식 클래스 메소드가 부모 클래스 메소드와 같거나 보다 더 공개되는 방향으로 변경되어야 한다.

오버라이딩 예

class SubTest1 {
    public void printInt(int val) {
        System.out.println("subTest1 class | value is = " + val);
    }

    public Object printval(Object val) {
        System.out.println("subTest1 class | value is = " + val.toString());
        return val;
    }

    void printString(String str) {
        System.out.println("subTest1 class | value is = " + str);
    }
}

class SubTest2 extends SubTest1 {
    @Override
    public void printInt(int val) {
        System.out.println("subTest2 class | value is = " + val);
    }

    @Override
    public String printval(Object val) {
        System.out.println("subTest2 class | value is = " + val.toString());
        return val.toString();
    }

    @Override
    public void printString(String str) {
        System.out.println("subTest2 class | value is = " + str);
    }

}

리턴 타입이 참조 자료형일 경우 자식 클래스의 메소드의 리턴 타입은 부모 클래스 메소드의 리턴 타입의 하위 클래스 자료형을 리턴해도 성립한다.

static 메소드 오버라이딩?

class SubTest1 {
    public static void printInt(int val) {
        System.out.println("subTest1 class | value is = " + val);
    }
}

class SubTest2 extends SubTest1 {
    public static void printInt(int val) {
        System.out.println("subTest2 class | value is = " + val);
    }
}

public class Test {
    public static void main(String[] args) {
        // SubTest1 클래스에 SubTest2 클래스 객체 할당
        SubTest1 subTest1 = new SubTest2();
        subTest1.printInt(1);
        SubTest2 subTest2 = new SubTest2();
        subTest2.printInt(1);
    }
}
subTest1 class | value is = 1
subTest2 class | value is = 1

위 코드는 static 메소드인 printInt를 override한 뒤 호출한 결과이다. 분명 SubTest2 클래스 객체를 할당했으니 오버라이드하게끔 작성된 메소드 호출 결과로는 SubTest2 클래스의 printInt()의 결과만 출력되어야 했으나 변수가 갖는 클래스 값에 따라 결과가 달라짐을 볼 수 있다. 이렇듯 오버라이딩이 성립하려면 우측의 변수에 할당된 클래스 객체에 따라 호출할 메소드가 결정되어야 하는데 좌측의 컴파일 시 결정된 변수 클래스가 갖는 메소드를 호출하는 문제가 발생하여 오버라이딩이 성립하지 않는다. 오버라이딩이 성립하지 않았기에 이 현상을 오버라이딩이라 부르지 않고 메소드 하이딩(Hiding)이라 부른다. 에러를 출력하지 않으나 다형성을 해칠 수 있으므로 이런 방식의 코딩은 지양하는 게 좋다.

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

java filename 명령어 실행 시 확장자 제거 이유

javac filename.java 명령어를 수행하면 filename.class라는 바이트 코드가 담긴 바이너리 파일이 생성된다. 이 파일을 JVM에 전달하여 실행하기 위해서는 java filename 명령어를 실행해야 하는데 그 이유는 JVM이 file을 실행한다고 표현하지 않고 file이름을 가진 바이트코드에 main 메소드가 존재한다는 사실을 전달하는 의미이기 때문이다.

따라서 filename.class를 java.exe 명령어의 인자로 넘기면 filename을 package로 인식하고 class 확장자를 .class 파일의 파일명으로 인식하여 main 메소드가 없다는 에러나 메인 스레드의 메인 메소드를 찾을 수 없다는 NoClassDefFoundError를 출력하게 될 것이다.

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

문제 제기

흔히 float나 double 연산 시 오차가 발생하여 숫자에 민감한 금융 거래 등에는 BigDecimal을 사용해야 한다고 설명한다. 보통 그 이유를 컴퓨터는 "정확히" 실수 표현이 불가능해 근사 과정에서 연산과정 중 오차가 쌓임에 따라 최종적으로 가시적인 오차가 발생한다고 말한다.

그렇다면 좀 더 명확하게 그 이유를 찾아보고 BigDecimal 라이브러리는 왜 정확성 보장이 가능한 지 알아보고자 한다.

float와 double의 오차

float, double 오차 표시 코드

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        double doubleValue = 0.0d;
        for (int i = 0; i < 100; i++) {
            doubleValue += 0.1d;
        }
        double doubleResult = 0.1d * 100;
        System.out.println("expected value is : " + doubleResult + " result is : " + doubleValue);
        float floatValue = 0.0f;
        for (int i = 0; i < 100; i++) {
            floatValue += 0.1f;
        }
        float floatResult = 0.1f * 100;
        System.out.println("expected value is : " + floatResult + " result is : " + floatValue);

    }

}

연산 결과

expected value is : 10.0 result is : 9.99999999999998
expected value is : 10.0 result is : 10.000002

위 코드의 결과처럼 소수점 연산은 반복하다보면 원하던 결과와 사뭇 다른 값을 얻게 된다. 이러한 정확성 손실로 인해 금전 거래 시 조심해야 함을 항상 말한다. 물론 소수 연산이 필요없다면 정수 자료형으로 연산하겠으나 금융거래는 %연산이 빈번히 이루어지니 소수를 항상 고려해야 한다.

실수 자료형의 오차 - 부동소수점

실수 표현에서의 오차는 부동소수점 표현에 기인한다. 자리수가 굉장히 크거나 무한 소수 처리에 있어 적절한 표기를 위해 표준에 맞춰서 근사값을 다루기 때문이다. 일례로 10진수 기반일 경우 분모의 원소가 2, 5로만 구성되면 유한소수이나 나머지 인수가 들어가면 무한소수가 되어 표기를 위해 적절히 반올림 처리를 했다. 이는 컴퓨터에서도 마찬가지로 2진수 기반인 경우 분모에 2를 제외한 인수가 포함되면 무한소수가 된다.

예를 들어 0.1f를 표현하려면 BigDecimal 표기 기준 0.100000001490116119384765625을 근사하고 있는데 이걸 비트로 표현하면 1100이 무한히 반복된다. 적당히 잘라서 표현하면 (e = −4 s = 110011001100110011001101)이 된다.

BigDecimal, BigInteger

BigDecimal은 double보다 범위가 약 2배 이상 길어 좀 더 정밀한 연산이 가능하다. 그리고 근사값이 아니라 유효숫자와 소수점 숫자를 기준으로 표현한다. 따라서 소수 연산이나 금융 거래 시 BigDecimal이나 BigInteger를 써야한다.

BigDecimal 사용 시 주의할 점

소수부 표현이 정확하다고 BigDecimal foo = new BigDecimal(0.1);을 넣어버리면 생성자 내부에 자바가 0.1을 근사한 값을 넣어버려 실제로는 0.1과는 완전히 다른 값인 0.1000000000000000055511151231257827021181583404541015625을 저장해버린다. 따라서 BigDecimal을 사용해 소수연산을 할 때는 반드시 String으로 변환하여 근사할 여지가 없게끔 처리해야 한다.

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal bigDouble = new BigDecimal(0.1);
        BigDecimal bigString = new BigDecimal("0.1");
        System.out.println("BigDecimal(0.1) value is : " + bigDouble);
        System.out.println("BigDecimal(\"0.1\") value is : " + bigString);
    }
}
BigDecimal(0.1) value is : 0.1000000000000000055511151231257827021181583404541015625
BigDecimal("0.1") value is : 0.1

precision과 scale

precision은 유효숫자 개수를 의미하며 scale은 소수 자리의 개수를 의미한다. 즉, 예를 들어 12.345는 precision이 5이고 scale이 3이 된다.

19/100 = 0.19 // integer=19, scale=2
21/110 = 0.190 // integer=190, scale=3

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

문제 제기

자바를 공부하던 중 왜 메소드를 클래스 안에 넣어야만 하는가에 대해 궁금했다. 내용을 찾던 중 javac compiler가 .java 파일을 .class 파일로 변환하는 과정에 기인함을 알게되었고 겸사겸사 Java의 .class 파일 구조에 대해 작성하고자 한다.

java 코드에서 메소드가 클래스에 속해야 하는 이유

java 코드를 javac compiler로 컴파일할 때 컴파일러는 java 코드 내 class 블록을 찾아서 각각의 .class 파일로 구분하여 byte코드를 생성한다. 즉, 메소드가 class 블록 내에 있지 않으면 해당 메소드에 대한 상위 클래스를 찾을 수 없어 컴파일 에러가 발생한다.

Development_Java_class_file_structure_001.png

위 그림과 같이 한 소스 파일 내 여러 클래스 정의 후 컴파일 수행 시 코드에 정의한 class 개수만큼 .class 파일이 생성됨을 알 수 있다.

java .class 파일 구조

  1. magic_number : JVM이 valid한 compiler로부터 .class 파일이 생성되었는지 검증하는 값
  2. major_version & minor_version : .class 파일의 컴파일 버전을 의미한다. 낮은 버전의 컴파일러에서 생성된 .class 파일은 상위 버전에 JVM에서 구동할 수 있으나 그 반대는 runtime exception을 출력하여 구동이 불가하다.
    • .class 파일을 생성한 javac 컴파일러 버전 <= JVM 버전이어야 구동이 가능
  3. constant_pool_count : .java 파일 컴파일 시 모든 변수와 메소드 참조 주소는 constant pool에 symbolic reference로 저장된다.
  4. constant_pool[] : 변수, 메소드 정보를 담은 symbolic reference 배열
    • symbolic reference : 클래스의 특정 메모리 주소를 참조 관계가 아니라 참조하는 대상의 이름만을 지칭
    • 이름에 맞는 객체의 주소를 찾아서 연결하므로 실제 메모리 주소가 아니라 이름만을 갖게되는 것
  5. access_flag : class file에 선언된 제어자 정보
  6. this_class : class file의 정보
  7. super_class : 현재 클래스의 상위 클래스(extend한 클래스)를 의미 (없으면 Object가 상위)
  8. interface_count : 현재 class file이 implements한 interface 개수
  9. interface[] : implements한 interface들의 정보를 담은 배열
  10. fields_count : 현재 class file에 선언된 static 변수(클래스 변수) 개수
  11. fields[] : 클래스 변수 정보를 담은 배열
  12. method_count : 현재 class file에 선언된 메소드 개수
  13. method[] : 선언된 메소드 정보를 담은 배열
  14. attributes_count : 현재 class file에 선언된 변수(인스턴스 변수) 개수
  15. attributes[] : 인스턴스 변수 정보를 담은 배열

+ Recent posts