데브코스 - [스터디] java/cs 기술 질문 - week2

Heesu Song·2025년 3월 24일

데브코스 - 백엔드

목록 보기
12/32
post-thumbnail

String, StringBuilder, StringBuffer를 설명하고, 각각의 차이에 대해 설명해주세요.

String (불변)

개념

  • String은 불변 자료형으로 한성 생성되면 변경할 수 없는 객체이다.
  • 문자열을 변경하는 경우에는 새로운 객체가 생성된다.

특징

  • 불변
  • Thread-Safe: 멀티스레드 환경에서도 안전하게 사용이 가능하다.
  • 문자열을 자주 수정하면 메모리 낭비가 발생할 수 있다.

String 객체의 내부 구성 요소

public final class String implements java.io.Serializable, Comparable {
	private final byte[] value;
}
  • 인스턴스 생성 시에 생성자의 매개변수로 입력받는 문자열은 value에 문자형 배열로 저장되는데 value가 상수형이기 때문에 값을 바꾸지 못하는 것

자바 언어에서 String을 불변으로 설정한 이유

  1. 보안
    • String이 가변이라면 변경이 가능해져서 보안에 문제가 생길 수 있다.
    • 예를들어, 비밀번호/연결 URL등 이 String으로 저장될 때 String이 불변 객체가 아니라면 외부에서 조작할 가능성이 있다.
  2. 성능 최적화(캐싱)
    • String을 Heap 메모리의 String Pool에 저장해서 중복을 방지한다.
    • 불변 객체라 여러 곳에서 재사용이 가능하므로 메모리 사용량을 줄일 수 있다.
  3. Thread-safe
    • String은 불변 객체이기 때문에 동기화 없이 안전하게 공유 가능하므로 멀티 스레드 환경에 안전하다. → 만약 여러개의 스레드가 String을 공유할 때 가변 객체라면 동기화 문제가 발생할 수 있다.
  4. 해싱 성능 향상
    • String이 불변 객체라 한번 해시코드를 계산하면 변경되지 않는다.
    • 따라서 HashMap, HashSet 등에서 검색 속도가 빠르다.

StringBuilder(가변)

개념

  • StringBuilder는 가변 문자열을 다룰 수 있는 클래스이다.
  • String과 다르게 새로운 객체를 생성하지 않고 기존 객체를 수정한다.
  • StringBuffer와 유사하지만, StringBuilder는 멀티스레드 환경에서 안전하지 않다.

특징

  • 가변
  • Thread-Unsafe: 동기화를 미지원하기 때문에 멀티스레드 환경에서 안전하지 않다.
  • String보다 빠르게 문자열을 수정할 수 있기 때문에 성능이 좋다.

StringBuffer

개념

  • StringBuffer 또한 가변 문자열을 다룰 수 있는 클래스이다.
  • StringBuffer 클래스의 메서드와 StringBuilder 클래스 메서드의 사용법은 동일하다.
  • .append(), .delete() 등의 API를 이용하여

특징

  • 가변
  • Thread-safe: synchronized를 사용하기 때문에 멀티스레드 환경에서 안전하다.
  • synchronized 때문에 StringBuilder보다 성능이 떨어진다.

StringBuffer 객체의 내부 구성 요소

public final class StringBuffer implements java.io.Serializable {
	private byte[] value;
}
  • String과 다르게 value를 선언할 때 상수(final)키워드가 없기 때문에 값을 변경하는게 가능

String, StringBuilder, StringBuffer의 차이

특징StringStringBuilderStringBuffer
가변성불변가변가변
성능느림(새 객체 생성)가장 빠름느림(동기화 비용)
Thread-safe멀티 스레드 환경에 안전멀티 스레드 환경에 안전하지 않음멀티 스레드 환경에 안전
멀티스레드사용 가능하지만 비효율적동기화가 안되기 때문에 사용 불가능사용 가능
  • String → 문자열 변경이 거의 없는 경우에 사용하면 좋다.
  • StringBuilder → 문바열 변경이 자주 발생하는 경우에 사용하면 좋다.
  • StringBuffer → 멀티 스레드 환경에서 문자열을 변경해야 하는 경우에 사용하면 좋다.

자바의 final 키워드에 대해 설명해주세요(final, finally, finalize())

final

  • final은 자료형에 값을 단 한 번만 설정할 수 있게 강제하는 키워드이다. 값을 한 번 설정하면 그 값을 다시 설정할 수 없다. final은 변수, 메서드, 클래스에 사용된다.
  • 변수에 사용 되면 해당 변수는 한 번 값이 할당 된 후 변경이 불가능해지고, 메서드에 사용 되면 하위 클래스에서 오버라이딩이 불가능해진다. 클래스에 사용 된다면 상속이 불가능 해진다.

값 변경, 메서드 오버라이딩, 상속을 방지하는 역할을 한다.


finally

  • finally는 예외처리에서 사용 되는 구문으로, 예외처리 발생여부를 떠나 코드가 무조건 실행 되도록 한다.
 try {
        ① 예외가 발생할 수 있는 코드;
   } catch (발생할 수 있는 예외 타입) {
        ② 예외처리 코드;
   } finally {
        ③ 예외와 상관없이 무조건 실행되는 코드;
   }

finalize()

  • finalize 메서드는 Object 클래스에 정의된 가비지 컬렉터가 객체를 제거하기 직전에 호출되는 메서드이다.
  • 일반적으로 객체의 자원을 해제하거나 정리할 때 등 마무리 작업을 수행하는데 사용된다.
  • java 9 이후로 deprecated 되어 더 이상 사용 되지 않는다.
  • GC의 실행 시점은 JVM이 관리하기 때문에 finalize가 언제 호출될지 확실하지 않다.

예시

protected void finalize() throws Throwable {
  // 객체 소멸 전에 필요한 처리
  super.finalize();
}

자바에서 다루는 예외 2가지와 예외를 처리하는 3가지 방법에 대해 설명해주세요.

자바의 예외 (프로그램 실행 중에 발생하는 오류)

체크 예외 (Checked Exception) / 언체크 예외 (Unchecked Exception)

체크 예외

  • RuntimeException 클래스를 상속받지 않지만 Exception 클래스는 상속 받는 예외 클래스이다.
  • 체크 예외는 복구 가능성이 있는 예외이기 때문에 반드시 예외를 처리하는 코드를 작성해 주어야 한다.(try-catch, throw 등)
  • 주로 외부 환경과의 상호작용(파일, 네트워크, DB 등) 에서 발생한다.

대표적인 체크 예외

  • IOException (입출력 오류)
  • SQLException (DB관련 오류)

체크예외는 컴파일러가 예외처리를 강제하는 예외이기 때문에 개발자가 실수로 예외처리를 누락하지 않도록 도와준다.

→ 하지만 개발자가 모든 체크 예외를 처리해줘야 하고 신경쓰고 싶지 않은 예외까지 처리해야 된다는 단점이 있다.

실제 애플리케이션을 개발할 때 발생하는 에외 들은 복구가 불가능한 경우가 많기 때문에 대부분 언체크 예외를 사용한다.


언체크 예외

  • 예외를 처리하지 않아도 컴파일은 가능하지만 실행중에 오류를 발생시킨다.
  • RuntimeException 클래스를 상속받아 컴파일러가 예외처리를 강제하지 않는다.
  • 주로 프로그래밍의 오류로 발생(NULL, 배열 범위 초과 등)

대표적인 언체크 예외

  • NullPointerException
  • IllegalArgumentException

예외 처리 방법

예외 복구, 예외 처리 회피, 예외 전환

예외 복구

  • 예외가 발생했을 때 문제를 해결해 프로그램이 정상적으로 계속 실행 되도록 돌려놓는 방법이다.
  • try-catch 블록을 사용하여 예외를 처리하고, 반복문을 이용해서 예외가 발생하더라도 일정 수 만큼 재시도를 해서 프로그램이 중단 되지 않도록 한다.

예외 회피

  • 예외를 직접 처리하지 않고 throws 키워드를 사용해 호출한 곳으로 예외를 던져 회피하는 방법이다.
  • 호출한 쪽에서 예외를 처리하는것이 더 적절할 때 사용한다.

예외 전환

  • 예외를 그대로 전달하는게 아니라 적절한 예외로 변환하여 던지는 방법이다.
  • 비즈니스 로직과 관련된 예외를 던질 때 주로 사용 된다.
  • 원래 발생 됐던 예외를 포함하여 새로운 예외를 생성할 수 있다.

ex) catch 블록에서 새로운 예외 던지기

public class ExceptionTranslation {
    public static void main(String[] args) {
        try {
            validateAge(-5);
        } catch (IllegalArgumentException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }
    }

    public static void validateAge(int age) {
        try {
            if (age < 0) {
                throw new ArithmeticException("나이는 음수가 될 수 없습니다."); // 원래 예외 발생
            }
        } catch (ArithmeticException e) {
            throw new IllegalArgumentException("잘못된 나이 입력: " + age, e); // 의미 있는 예외로 변환
        }
    }
}

equals()와 hashcode()에 대해 설명해주세요.

equals와 hashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다. 그렇기 때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있다. 두 메서드 모두 객체의 동등성 비교와 컬렉션에서의 활용을 위해 매우 중요한 메서드이다.

equals()

  • 두 개의 객체의 내용(값)이 같은지 비교하기 위한 메서드로 boolean equals(Object obj)로 정의 되어 있다.
  • equals가 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것이며, 이는 동일성(Identity)을 비교하는 것이다.
  • equals를 오버라이딩해서 논리적 동등성을 비교할 수 있다.

equals() 기본 구현

public boolean equals(Object obj) {
    return (this == obj);
}

equals() 오버라이딩

class Person {
    String name;
    int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false; 
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name); 
    }
}

public class EqualsTest {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);

        System.out.println(p1 == p2);       // false (주소 비교)
        System.out.println(p1.equals(p2));  // true (내용 비교)
    }
}

→ 동일성을 비교하는 equals 메소드를 호출해보면 true가 나오는데, 그 이유는 String 클래스에서 equals 메소드를 오버라이드하여 객체가 같은 값을 갖는지 동등성(Equality)을 비교하도록 처리했기 때문이다.

hashCode()

  • 객체를 해시 기반 Collection(Set, Map)의 키로 사용할 때 필요한 해시 값을 반환하는 메서드
  • 논리적으로 같은 객체는 같은 해시코드 값을 가진다.
class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        // 객체 인스턴스마다 각기 다른 주해시코드(주소))를 가지고 있다.
        System.out.println(p1.hashCode()); // 622488023
        System.out.println(p2.hashCode()); // 1933863327
    }
}

hashCode() 정의

public native int hashCode();

→ native 키워드는 메소드가 JNI(Java Native Interface)라는 native code를 이용해 구현되었음을 의미한다.

hashCode() 오버라이딩

import java.util.Objects;

class Person {
    String name;
    int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // name과 age를 기반으로 해시코드 생성
    }
}

public class HashCodeTest {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);
        
        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true (같은 객체로 인식)
    }
}

→ 객체의 주소가 아닌 객체의 필드의 값을 비교하기 위해 equals() 를 오버라이딩 시킨다면 당연히hashCode도 같이 객체의 필드를 다루도록 오버라이딩 해야된다. 왜냐하면 equals() 의 결과가 true 인 두 객체의 해시코드는 반드시 같아야한다는자바의 규칙 때문이다.

equals()와 hashCode()의 관계

  • equals()가 ture 라면 동일한 객체라는 의미이고 이는 동일한 메모리 주소를 가진다는 의미이기 때문에 hashCode()의 값도 같아야 한다.
  • 앞서 hashCode() 오버라이딩에서 설명한거와 같이 equals메소드를 오버라이드 한다면 hashCode 메서드도 함께 오버라이드 되어야 한다.
  • 하지만 hashCode()가 같다고 해서 equals()가 반드시 true는 아니다. → 해시 충돌 때문
    • hashCode()는 32비트 정수값(int) 을 반환하므로, 서로 다른 객체라도 같은 hashCode()를 가질 가능성이 있디.
    • 하지만 equals()는 객체의 내용이 완전히 동일해야만 true를 반환하므로, hashCode()가 같아도 equals()가 다를 수 있다.

hashCode()는 객체를 빠르게 찾기 위한 값일 뿐 equals()가 true인지는 직접 확인하는게 좋다.

SOLID(객체지향 5대원칙)에 대해서 설명해주세요.

좋은 객체 지향 설계의 5가지 원칙(SOLID)

  • SRP: 단일 책임 원칙
  • OCP: 개방 폐쇄 원칙
  • LSP: 리스코프 치환 법칙
  • ISP: 인터페이스 치환 법칙
  • DIP: 의존관계 역전 원칙

SRP

Single Responsibility Principle

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 변경이 필요할 때 파급효과가 작으면 SRP를 잘 따른 것

OCP

Open / Close Principle

  • 소프트웨어 요소는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
  • 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요함

LSP

Liskov Substitution Principle

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위타입의 인스턴스로 바꿀 수 있어야 한다.
  • 즉 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있어여 한다.

ISP

Interface Segregation Principle

  • 객체는 자신이 사용하는 메서드에만 의존해야 한다.
  • 해당 인터페이스를 사용하는 객체를 기준으로 잘게 분리되어야 한다.

DIP

Dependency Inversion Principle

  • 구현 클래스에 의존하지 않고 인터페이스에 의존하여야 한다.

스프링과 OCP, DIP

  • 스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원
    • DI(Dependency Injection): 의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 코드의 변경 없이 기능 확장

원시 타입과 참조 타입에 대해 성능 관점, 메모리 관점, NULL 관점, 제네릭 관점에서 비교해주세요.

자바에서 데이터를 저장할 때 원시타입, 참조 타입 두가지 방식으로 관리한다.

  • 원시 타입은 정수, 실수, 문자, 논리 리터럴등의 실제 데이터 값을 저장하는 타입이다
  • 참조 타입은 객체(Object)의 주소를 참조하는 타입으로 메모리 번지 값을 통해 객체를 참조하는 타입이다.

원시 타입

  • stack 메모리 공간에 값 자체가 저장되며 boolean, int, long, char 등이 있다.
  • 원시타입의 객체는 비객체 타입이기 때문에 NULL값을 가질 수 없다.

참조 타입

  • 참조 타입은 원시 타입을 제외한 타입들(문자열, 배열, 열거, 클래스, 인터페이스)을 말한다.
  • heap 메모리 공간에 저장되며 해당 객체들의 주소를 stack 메모리 공간에 저장한다.
  • NULL 값을 가질 수 있으므로 NullPointerException이 발생할 수 있다.

성능 관점

  • 원시 타입은 스택에 메모리가 저장되기 때문에 접근 속도가 빠르다.
  • 참조 타입은 힙 메모리를 할당하고 객체를 생성해야 하기 때문에 원시 타입에 비해 상대적으로 접근 속도가 느리다.
  • 또한 원시 타입은 가비지 컬렉션의 영향을 받지 않지만 참조 타입은 가비지 컬레션에 의해 정리된다.

→ 성능 관점에서는 원시 타입의 성능이 참조 타입보다 우수하다.


메모리 관점

  • 원시 타입은 스택 메모리에 저장되며, 참조 타입은 힙 메모리에 저장된다.
int a = 10;          // 원시 타입 → 스택에 직접 저장된다.
Integer b = 10;      // 참조 타입 → 힙에 Integer 객체 생성 후에 저장된다.
  • 원시 타입에 비해 참조 타입이 사용하는 메모리 크기가 훨씬 크다.
원시타입이 사용하는 메모리참조타입이 사용하는 메모리
boolean - 1bitBoolean – 128 bits
byte - 8bitsByte - 128bits
short, cagr - 16bitsShort, Charater - 128bits
int, float - 32bitsInteger, Float - 128bits
long, double - 64bitsLong, Double - 196bits

→ 메모리 관점에서도 원시 타입이 참조 타입보다 효율적이다.


NULL 관점

  • 원시 타입은 NULL 값을 가질 수 없고, 값이 없으면 디폴트 값을 반환한다. (0, false 등)
  • 참조 타입은 NULL 값을 가질 수 있기 때문에 NullPointerException이 발생할 수 있다.
int x = null; // 컴파일 에러 발생 
Integer y = null; // 문제 없음 

→ 그렇기 때문에 참조 타입을 사용할 때는 반드시 NULL을 체크 해야한다.


제네릭 관점

제네릭에서는 원시 타입을 직접 사용할 수 없고 반드시 참조 타입을 사용해야 한다.

// 불가능
List<i> list;

// 가능
List<Integer> list;
profile
Abong_log

0개의 댓글