728x90

1. Static이란??

Static 키워드를 선언하게 되면 사용할 때만 메모리에 할당하고, 사용 안 할 때는 제거되는 일반 선언들과 달리 프로그램이 종료될 때 까지 메모리에 할당되어 있는 것을 의미합니다.

 

그렇기 때문에 우리가 Static으로 선언한 것들은 따로 생성하지 않고 가져다 사용할 수 있는 것입니다.

 

그렇다면 우리가 흔히 사용하는 Static 변수와 Static 메소드들은 어떤 작동원리를 가지고, 어떤 차이점이 존재하는지 알아보도록 하겠습니다.


2. Static 변수

Static 변수는 클래스 수준에서 선언되며, 인스턴스와 관계없이 모든 객체가 동일한 메모리 공간을 공유합니다. Static 변수는 클래스가 메모리에 로드될 때 한 번만 초기화되며, 프로그램이 종료될 때까지 메모리에 유지됩니다.

Static 변수의 특징

1. 공유 메모리
Static 변수는 클래스 로더가 클래스를 로드할 때 메모리에 할당되며, 해당 클래스의 모든 인스턴스가 이 변수를 공유합니다. 따라서 인스턴스마다 별도의 값을 가지지 않고, 모든 인스턴스가 동일한 값을 참조합니다.

 

2. 클래스 이름으로 접근 가능
Static 변수는 객체를 생성하지 않고, 클래스 이름을 통해 직접 접근할 수 있습니다.

 

3. 메모리 효율성
여러 객체에서 공통으로 사용하는 데이터를 Static 변수로 선언하면, 각 객체가 별도의 메모리를 할당받지 않으므로 메모리 효율성을 높일 수 있습니다.

 

4. 초기화 시점
Static 변수는 프로그램 실행 시점에 클래스가 메모리에 로드될 때 초기화됩니다. 한 번만 초기화되며, 이후 변경된 값은 프로그램 종료 시점까지 유지됩니다.


Static 변수 사용 사례

1. 공유 데이터 관리
객체생성과 관계없이 데이터를 공유하고 싶다면 Static변수를 사용하여 관리할 수 있습니다.

 

2. 상수 선언

변하지 않는 상수 값을 정의할 때 Static 변수를 final 키워드와 함께 사용하여 메모리 낭비를 줄일 수 있습니다.

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
}


3. 유틸리티 클래스에서 공용 변수 사용
특정 설정 값이나 공용 데이터를 유틸리티 클래스에 Static 변수로 선언해 다른 클래스에서 쉽게 접근하도록 합니다.

public final class StandardCharsets {
    private StandardCharsets() {
        throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
    }
    public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
    public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE;
    public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE;
    public static final Charset UTF_16BE = new sun.nio.cs.UTF_16BE();
    public static final Charset UTF_16LE = new sun.nio.cs.UTF_16LE();
    public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
}

3. Static 메소드

Static 메소드는 객체를 생성하지 않고도 호출할 수 있는 클래스 수준의 메소드입니다. Static 변수와는 다르게 메소드는 로직을 정의하며, Static 키워드로 선언된 메소드는 클래스의 모든 객체가 동일한 동작을 공유합니다.

Static 메소드의 특징

1. 객체 없이 호출 가능
Static 메소드는 클래스 이름으로 호출할 수 있습니다.

 

2. Static 변수와 연동 가능
Static 메소드는 같은 클래스에 선언된 Static 변수에 직접 접근할 수 있습니다.

 

3. 인스턴스 멤버 사용 불가
Static 메소드 내에서는 클래스의 인스턴스 변수나 인스턴스 메소드를 직접 사용할 수 없습니다. 이는 Static 메소드가 클래스 수준에서 동작하기 때문입니다. 인스턴스 멤버를 사용하려면 해당 메소드에서 객체를 생성하거나 참조를 전달받아야 합니다.

 

4. 상속과 재정의 제한
Static 메소드는 상속받은 클래스에서 오버라이드(재정의)할 수 없습니다. 하지만 동일한 이름으로 정의해 숨길 수는 있습니다.


Static 메소드 사용 사례

1. 유틸리티 메소드 제공
자주 사용되는 공용 로직들을 Static 메소드로 정의해 간편하게 호출할 수 있습니다.

 

2. 팩토리 메소드 구현
특정 객체를 생성하는 로직을 Static 메소드로 제공할 수 있습니다.

 

3. 프로그램의 진입점
Java 프로그램은 main 메소드를 Static으로 선언하여, 객체를 생성하지 않고 프로그램의 실행을 시작합니다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

728x90
728x90

이번 문제 또한 매우 간단한 문제입니다.

N명의 학생이 존재하고, 3번째 줄부터 N개 나오는 학생들 이름을 가지고 인기도를 측정하여 출력하면 되는 문제입니다.

 

우선 여기서 봐야 할 점은 인기도 측정 방식정렬인데요.

인기도 측정 방식은 그냥 나오는 학생 이름들을 전부 인기도에 반영하면 되는 간단한 문제이고,

정렬은 주어진 조건에 맞춰 정렬하면 됩니다.

 

정렬의 경우 저는 Stream의 sorted함수를 사용하였는데요. 이때, sort조건을 커스텀함으로써 간단하게 정렬하였습니다.

이후 reduce를 활용하여 StringBuilder에 값들을 담게 했는데요, 이는 String과 달리 StringBuilder는 불변성이 아니어서 메모리 활용 측면에서 훨씬 효율적이라 판단하여 사용하였습니다.

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        Map<String, Integer> map = new HashMap<>();
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());

        for(int i = 0; i < N; i++) {
            map.put(st.nextToken(), 0);
        }

        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            while (st.hasMoreTokens()) {
                String to = st.nextToken();
                map.put(to, map.get(to) + 1);
            }
        }

        StringBuilder answer = map.entrySet().stream().sorted((prev, now) -> {
            if(prev.getValue() != now.getValue()) return now.getValue() - prev.getValue();
            return prev.getKey().compareTo(now.getKey());
        }).reduce(new StringBuilder(),(sb, entry) -> sb.append(entry.getKey()).append(" ").append(entry.getValue()).append("\n"),StringBuilder::append);
        System.out.println(answer);
    }
}
  • 시간복잡도: O(n)
  •  공간복잡도: O(n)
728x90

'알고리즘 문제풀이' 카테고리의 다른 글

[ 백준 - 11725 ] 트리의 부모 찾기  (0) 2024.11.12
[ 백준 - 29701 ] 모스 부호  (0) 2024.11.11
[ 백준 - 2358 ] 평행선  (2) 2024.11.09
[테코테코] 2주차 Stack  (0) 2024.09.23
Hackerrank 사이트 소개  (2) 2024.06.13
728x90

JPA에 대하여

 

우리가 JPA를 알기 위해서는 사전 지식으로 ORM이 무엇인지를 알아야 합니다.

이에 이 글은 ORM부터 알아본 후 JPA에 대해 다뤄보도록 하겠습니다.

ORM이란?

ORM( Object Ralational Mapping )은 단어를 풀어 해석하면 '객체 관계형 연결'이 됩니다.

이 기술은 애플리케이션과 데이터베이스 연결 시 기존에는 SQL언어를 애플리케이션 서버에서 직접 작성하였지만, 이를 서버에서는 객체로 정의하여 행위에 대한 Action을 하면  정의된 객체를 해석하여 행위에 필요한 SQL문을 작성하여 데이터베이스로 전달하는 말 그대로의 Mapping역할을 합니다.

 

이러한 ORM은 기존 Mybatis와 같은 기술을 사용하던 것을 '객체 지향'적으로 사용하기 위해 나온 기술이라고 봐도 무방 할 것이라 생각하는데, 어떤 탄생 배경이 있는지 알아보도록 하겠습니다.

ORM 탄생 배경

우리가 개발할 때 객체 지향 언어를 사용하여 개발을 하고, 관계형 데이터베이스를 사용하게 되면 프로그램 내에서 데이터베이스와 연결하여 SQL문을 직접 작성을 해줘 데이터를 얻거나 작업을 할 수 있습니다.

 

이때 우리는 객체 지향 언어의 데이터 표현 방법관계형 데이터베이스의 데이터 표현 방법의 차이를 확인해 볼 필요가 있습니다.

 

객체 지향 언어은 데이터를 객체에 상호 연결된 그래프로 표현을 하고, 관계형 데이터베이스는 데이터를 표형식으로 표현을 하게 됩니다.

이렇게 표현 방식이 다른 두 개념을 한 프로그램에서 사용을 하려면 표형식 표현방법을 객체에 상호 연결된 그래프 방식으로 변경하는 작업을 하게 되는데, 이는 개발자에게 번거로움과 오류 발생의 원인을 제공하게 됩니다.

 

이리하여 관계형 데이터베이스와 연결하여 SQL문을 작성하는 부분을 추상화시켜버리게 되는데 이것이 ORM입니다.

ORM의 장단점

위 설명만 보면 ORM을 만능으로 볼 수도 있습니다.

개발자의 번거로움을 줄여주고!! 오류 발생의 원인을 없애 주다니!! 하고 말입니다.

하지만 대부분의 기술이 그렇듯 장단점이 존재합니다.

장점

  • 사용하기 편하다.  - 강점 -
  • 직접적인 SQL문 작성이 없는 만큼 신경써야하는 부분이 줄어든다.
  • 객체를 정의하여 사용하고 얼마든지 수정 및 확장이 쉬워 유지보수가 편하다.

단점

  • ORM만 사용하여서는 복잡한 작업( 조회 혹은 수정 등 )을 대응하기 힘들다.
  • 객체 지향적이라 객체로 관리하게 되었다고 설계를 신경써서 하지 않으면 어떤 프로그램보다 복잡하고 힘든 프로그램이 만들어진다.

데이터 베이스를 객체 지향적으로 접근할 수 있는 만큼 설계의 중요성은 이루 말할 수 없을 정도이며,

사용하다 보면 그냥 SQL사용해서 개발하고 싶다라는 생각이 들 수도 있습니다.

다만 적절히 비즈니스 로직을 나눠 분석하여 사용할 경우보다 쉽게 개발할 수 있는 기술인 것에는 틀림없다 생각합니다.


JPA이란?

JPA(Java Persistent API)는 말그대로 JAVA Application에서 '객체와 관계형 데이터베이스 간의 매핑을 위한 API' 인터페이스의 모음입니다.

이 모음에는 3가지 구현체가 존재하는데, Hibernate, EclipseLink, DataNucleus가 있습니다.

Hibernate

하이버네이트는 JDBC API를 사용하는 JPA인터페이스 구현체입니다.

이는 무슨 뜻이냐 우리가 객체로 데이터를 CRUD 하게 되면 JPA 즉 하이버네이트 내부에서는 해당 객체를 분석하여 데이터베이스로 SQL문을 보내주게 되는데 이때 사용하는 API가 JDBC API라는 것입니다.

EclipseLink

EclipseLink는 JPA인터페이스 구현체입니다.

또한 JAXB, SDO를 구현한 포괄적인 오픈소스 프레임워크로, 다양한 기능과 확장성을 강력하게 제공하고 있습니다.

DataNucleus

DataNucleus는 JPA 인터페이스 구현체입니다.

EclipseLink와 마찬가지로 다른 데이터베이스들 또한 지원하고 있습니다.

Hibernate를 왜 많이 사용하는가

글을 작성하면서 의문이 들었던 점이 왜 Hibernate는 많이 사용하거나 본 거 같은데, EclipseLink와 DataNucleus는 다루는 글을 많이 못 본 것 같다는 생각을 하였습니다. 그리고 설명들을 보면 Hibernate는 JPA 인터페이스를 구현한 가장 대표적인 오픈소스라고 하는데 EclipseLink와 DataNucleus 또한 강력한 기능과 확장 가능성을 지원해 준 것을 확인할 수 있었습니다.

 

우선 찾아보며 생각한 것은 Hibernate의 강력한 커뮤니티와 그로 인해 나온 많은 선례, 또한 만들어진 지 오래된 만큼 안정성을 보장하기 때문이라 생각합니다.

JPA주요 특성

ORM

엔티티 클래스와 데이터베이스 테이블 간의 매핑을 지원합니다.

영속성 컨텍스트

데이터를 영속성 컨텍스트에 저장하고 지속적으로 추적하게 되는데, 이 데이터가 수정될 경우 트랜젝션이 걸려있지 않으면 바로 반영이 됩니다. 이게 무슨 의미인가 만약 데이터를 조회하여 엔티티로 받아서 바로 엔티티 속성을 수정한다면 실제 SQL문이 바로 날아가 변경된다는 뜻입니다.

 

즉 commit을 안 했는데 commit이 되는 현상으로 이러한 상황을 방지하기 위해서는 Transaction처리를 해주거나, 엔티티를 사용하지 말고 데이터를 수정할 때 사용하는 객체를 따로 만들어 해당 객체로 데이터 값들만 복사하여 사용하는 것이 안전한 사용법인 것 같습니다.

캐싱

영속성 컨텍스트는 한번 조회된 데이터들은 가지고있는데, 만약 조회되는 데이터가 영속성 컨텍스트에 저장되어있는 데이터라면 데이터베이스를 조회하지 않고 해당 데이터를 반환해줍니다. 이는 1차 캐시의 개념이며, 만약 1차 캐시에 없을 경우 2차 캐시를 조회하게되고, 거기에도 없다면 데이터베이스를 조회하게 됩니다.

 

이때 말하는 1차 캐시는 Hibernate에서 제공하는 영속성 컨텍스트 캐시이고, 2차 캐시는 Session Factory 캐시입니다.


Reference

 

Understanding EclipseLink

This chapter describes how to set up your JPA applications to work with a non-relational data source. There are many types of non-relational data sources. These include document databases, key-value stores, and various other non-standard databases, such as

eclipse.dev

 

 

JPA Getting Started Guide (v5.2)

Developing applications is, in general, a complicated task, involving many components. Developing all of these components can be very time consuming. The Java Persistence API (JPA) was designed to alleviate some of this time spent, providing an API to allo

www.datanucleus.org

 

 

What is Object/Relational Mapping? - Hibernate ORM

Idiomatic persistence for Java and relational databases.

hibernate.org

 

캐싱 참조자료: https://www.baeldung.com/hibernate-second-level-cache

 

728x90
728x90

이번에 소개할 문제는 문자열 압축이라는 알고리즘 문제이다.

 

해당 문제는 데이터를 처리할 때 최소한의 메모리를 사용하여 문자열을 저장하고 싶은 게 핵심인 문제인 거 같다.

 

예를 들어보면 "가나다라마바사아"라는 문자열이 존재한다고 가정해보자.

이때 해당 문자열을 그대로 저장하게 되면 8글자를 Byte로 저장하게 된다.

위와 같이 중복된 글자가 없으면 그대로 저장하는 것이 데이터의 정확도상 당연한 것이라 생각한다.

 

하지만 "가가가나나나나나나다다다다다라라라마마바"라는 문자열이 있다고 생각해보자.

위와 같은 문자열이 있을 때 해당 문자열을 그대로 저장하는 것은 20개의 글자를 Byte로 저장하는 것이다.

이럴 때 동일한 문자가 연달아 존재할 때 몇 개인지를 앞에 명시하여 보다 짧은 문자열로 저장하는 것이 훨씬 적은 데이터를 가지고 저장을 할 수 있다.

 

또한 문자열을 줄일 때 1 단어 단위로 줄이면 "3가6나5다3라2마바"와 같은 형태로 저장되는 것이고,

2 단위로 줄이면 "가가가나2나나나다2다다라라라마마바"라는 문자열로 저장되는 식으로 몇 개 단위로 잘라 저장하느냐에 따라 저장되는 데이터 양에 차이를 주게 된다.

 

다시 문제로 돌아가서 해당 문제는 데이터를 최소한 짧은 문자열로 저장하고 싶은데 n단위로 잘라 저장했을 때 가장 적을 경우 가장 짧은 문자열일 경우 해당 문자열의 length를 구하여라로 생각하면 될 거 같다.

 

우선 default를 지정하도록 하자.

int answer = s.length();

해당 default로 지정한 경우는 어떤 방법으로 문자열을 압축한다 하더라도 기본 문자열보다 길어질 수 없기 때문에 기본 문자열의 length를 default로 지정하였다.

 

이후 먼저 중요하게 생각한 것은 문자열을 자를 건데 몇 개 단위로 자를지에 관해 먼저 생각하기로 하였다.

for(int i = 1; i< s.length() /2 +1; i++) {

}

문자열을 자를 거지만 절반을 넘어갈 경우 어차피 default와 길이는 같을 테니 적절하게 max를 지정해 주었다.

 

이후 잘라 만들어진 문자열과 이전 값을 비교하기 위한 변수와 몇 번째 압축인지를 알기 위한 변수를 선언해 주었다.

// i 는 자르는 갯수
for(int i = 1; i< s.length() /2 +1; i++) {
    StringBuffer sb = new StringBuffer();

    //이전값
    String bef = "";

    // 압축 count default 1
    int zipNum = 1;
}

이후부터는 subString으로 문자열을 자르기 위해 반복문을 돌려 문자열을 자르는 startPoint와 endPoint를 지정해 주도록 하자.

for (int j = 0; j < s.length(); j++) {

    //마지막 index
    int endNum = i+j > s.length() ? s.length() : i+j;

    //현재 
    String sub = s.substring(j, endNum);

}

위에서 나온 sub의 값은 현재 잘려 나온 문자열의 값이다.

 

현재 잘려나온 값을 알았으니 이제 해당 값이 압축이 가능한 문자열인지 check를 해볼 차례이다.

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
				
        //마지막 index
        int endNum = i+j > s.length() ? s.length() : i+j;

        //현재 
        String sub = s.substring(j, endNum);
    
        if (!sub.equals(bef)) {
        
            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;
        }
    
    }
}

bef = 이전 값. 

sub = 현재 값.

위 두 값을 비교하여 같을 경우 압축 가능한 문자열이니 zipNum을 올려주며 끝나고,

문자열이 같지 않을 경우 압축할 수 없는 값이니 다음 단계를 진행해 주었다.

 

if (!sub.equals(bef)) {

    if(zipNum > 1) {
        sb.append(zipNum);
    }

    if (!bef.equals("")) {

        if (endNum != s.length()) {
            sb.append(bef);
        }
        else {
            sb.append(bef).append(sub);
        }	
    }

    bef = sub;
    zipNum = 1;
}
else {
    zipNum++;

    if (endNum == s.length()) {
        sb.append(zipNum).append(bef);
    }
}

자른 문자열이 이전과 이후가 같을 확률은 다를 경우보다 높아 if문 앞 조건에서 처리를 할 수 있도록 하였다.

 

zipNum이 1보다 클 경우 해당 문자열은 이미 앞에서 문자열 압축이 이뤄진 것이니 압축이 된 문자열 숫자를 붙여주고,

이전 문자를 붙여줌과 동시에 현재 잘린 문자열이 기존 문자열의 마지막 단계가 아닐 경우 bef에 값을 저장하여 이후 들어올 잘린 문자열이 현재 값과 비교를 할 수 있게 조절해준다.

 

문자열이 서로 같을 경우 zipNum을 1올려줌과 동시에 현재 문자열이 마지막 문자열인지 체크하여 값을 추가해주도록 하여 값이 빠지는 경우가 생기지 않게 해 주었다.

 

이대로 끝났으면 좋겠지만 아직 2가지를 더 처리를 해 주어야 한다.

 

우선 현재 문자열을 자를 때 j의 값을 사용하게 되는데 해당 값은 무조건 1씩 오르기 때문에 2개 이상의 단어를 자르게 될 시 값이 중복되어 나오는 경우가 생기게 된다.

 

설명을 하자면 본인이 원하는 경우는 1, 3, 5, 7, 9와 같이 2단계씩 건너뛰려 하는데 이 코드를 지금 상태로 사용하게 될 시 1, 2, 3, 4, 5, 6, 7, 8, 9와 같이 1단계씩 건너 2 단어씩 잘라 앞 문자열의 뒷 단어와 뒷 문자열의 앞 단어가 곂치는 현상이 발생하는 것이다.

 

이를 해결하기 위해선 

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
			
        int endNum = i+j > s.length() ? s.length() : i+j;

        if (!sub.equals(bef)) {
					
            if(zipNum > 1) {
                sb.append(zipNum);
            }

            if (!bef.equals("")) {

                if (endNum != s.length()) {
                    sb.append(bef);
                }
                else {
                    sb.append(bef).append(sub);
                }	
            }

            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;

            if (endNum == s.length()) {
                sb.append(zipNum).append(bef);
            }
        }
    
    	//단계를 조절하기 위해 값을 현재 end값 -1로 명시해준다.
        j = endNum - 1;
    }
}

j = 문자열 자르는 단계 (index)

 

j를 마지막 잘랐던 index번호로 변경해주면 반복문이 작동하며 j++로 인해 의도했던 번호보다 1이 많아지게 되어 -1을 해주면 정상적으로 작동하게 된다.

 

subString의 용도상 startIndex부터 ~ endIndex전까지 잘라주게 되는데 위와 같이 해주면 이전 문자열의 index 다음 index부터 문자열을 잘라주게 되어 값의 중복 없이 정상적으로 작동하게 된다.

 

마지막으로

answer = answer > sb.toString().length() ? sb.toString().length() : answer;

잘랐던 문자열을 toString()으로 하나의 문자열로 뽑아 보았을 때 해당 문자열의 길이가 이전에 잘라서 합친 문자열보다 짧을 경우 해당 값을 정답으로 제출하기 위해 비교하며 저장한다.

 


제출 코드

class Solution {
    public int solution(String s) {
    
        int answer = s.length();
	
		for(int i = 1; i< s.length() /2 +1; i++) {
        
			StringBuffer sb = new StringBuffer();			
			String bef = "";
			int zipNum = 1;
            
			for (int j = 0; j < s.length(); j++) {
				
				int endNum = i+j > s.length() ? s.length() : i+j;				
				String sub = s.substring(j, endNum);
                
				if (!sub.equals(bef)) {
					
					if(zipNum > 1) {
						sb.append(zipNum);
					}
					
					if (!bef.equals("")) {

						if (endNum != s.length()) {
							sb.append(bef);
						}
						else {
							sb.append(bef).append(sub);
						}	
					}
					
					bef = sub;
					zipNum = 1;
				}
				else {
                
					zipNum++;
					
					if (endNum == s.length()) {
						sb.append(zipNum).append(bef);
					}
				}
				j = endNum - 1;
			}
			answer = answer > sb.toString().length() ? sb.toString().length() : answer;
		}
		
        return answer;
    }
}

 


해당 문제를 풀면서 stream을 사용하며 풀거나 재귀 함수를 사용할 시 훨씬 코드가 줄어들게 되지만 아직 실력이 부족하기도 하고 외부 라이브러리를 사용하지  않고 직접 돌아가는 상황을 작성해보고 싶어 위와 같이 작성하게 되었다.

 

다음에 stream이나 재귀함수를 공부하며 Refactoring 해 보아야겠다.

728x90

+ Recent posts