Thread에 대해서 오래전에 정리했던 적이 있다. 하지만 일을 하다가.. 아직도 Thread에 대해서 잘모른다는 느낌이 들어서 더 자세하게 다시한번 정리를 하고자 한다.

 

https://codingstudy95.tistory.com/67

 

스레드

사전적 의미로 한 가닥의 실이라는 뜻으로 한가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름이므로

codingstudy95.tistory.com

 

 

스레드란 무엇인가!!?

자바에서 Thread(스레드)는 프로그램 내에서 동시에 실행되는 작업의 단위를 의미한다. 회사에서 여러 사람이 각자의 다른 일을 동시에 하는 것과 마찬가지로, 스레드는 하나의 프로그램 안에서 여러 작업을 동시에 처리할 수 있도록 해준다.

(예: 데이터 처리, 사용자 요청 처리, 파일 입출력 등)

  • 단일 스레딩 : 한 사람이 모든 일을 순서대로 처리하는 것 -> 모든 작업이 순차적으로 처리되므로, 하나의 작업이 오래 걸리면 다른 작업들도 지연된다.
  • 멀티스레딩 : 여러 사람이 동시에 각자 일을 분담해서 처리하는 것 -> 여러 스레드가 동시에 작업을 처리하여, 한 작업이 늦어도 다른 작업은 계속 진행될 수 있다.

 

 

그럼 자바에서는 왜 멀티스레딩이 필요할까?

 

식당에서 한 사람이 모든 요리를 한다면 주문이 많은 경우 오래 걸리겠지만, 여러 요리사가 동시에 각자 다른 요리를 준비하면, 음식이 빨리 준비되는것과 비슷하다.

 

사용자 경험 향상

한 번에 한 작업만 처리한다면, 사용자가 어떤 요청을 할 때마다 기다려야 한다. 하지만 멀티스레딩을 사용하면, 여러 작업이 동시에 처리되어 응답 속도가 빨라지고 사용자 경험이 개선된다.

 

자원 활용의 극대화

컴퓨터는 여러 CPU 코어를 가지고 있는데, 멀티스레딩을 통해 이 코어들을 동시에 사용할 수 있다. 즉, 컴퓨터의 능력을 최대한 활용하여 더 빠르고 효율적으로 작업할 수 있다.

 

 

자바에서 스레드를 만들어보자 자바에서는 스레드를 만드는 방법이 두 가지 있다.

 

1. Thread 클래스 상속하기

// MyThread.kt
class MyThread : Thread() {
    // run 메서드를 재정의하여 스레드가 실행할 작업을 정의합니다.
    override fun run() {
        // 스레드가 실행될 때, "Hello from MyThread!"를 5번 출력합니다.
        for (i in 1..5) {
            println("Hello from MyThread! - $i")
            // 잠깐 멈추는 시간 (1000밀리초 = 1초)
            Thread.sleep(1000)
        }
    }
}

fun main() {
    // MyThread 클래스의 인스턴스를 생성하고, start()를 호출하면 스레드가 실행됩니다.
    val thread = MyThread()
    thread.start()
}
  • MyThread는 Thread 클래스를 상속받아 만든 새로운 스레드 클래스이다.
  • run() 메서드 안에 스레드가 해야 할 일을 작성한다.
  • thread.start() 를 호출하면, 새로운 스레드가 시작되어 run() 메서드의 내용이 실행된다.

 

2. Runnable 인터페이스 구현하기

또 다른 방법은 Runnable 인터페이스를 구현하는 것이다. 이 방법은 클래스 상속의 제약을 피할 수 있다는 장점이 있다.

// MyRunnable.kt
class MyRunnable : Runnable {
    override fun run() {
        // 스레드가 실행될 때, "Hello from MyRunnable!"를 5번 출력합니다.
        for (i in 1..5) {
            println("Hello from MyRunnable! - $i")
            Thread.sleep(1000)
        }
    }
}

fun main() {
    // Runnable 인터페이스를 구현한 MyRunnable 인스턴스를 Thread에 전달하여 실행합니다.
    val runnable = MyRunnable()
    val thread = Thread(runnable)
    thread.start()
}
  • MyRunnable 은 Runnable 인터페이스를 구현하여, run() 메서드 안에 작업 내용을 정의한다.
  • 이 객체를 Thread 생성자에 넘겨주고, start() 를 호출하면 스레드가 실행된다.

 

스레드의 생명주기와 상태

자바 스레드는 여러 상태를 가진다. 각 상태는 스레드가 어떤 작업을 하고 있는지를 나타낸다.

 

  • New: 스레드가 생성되었지만 아직 실행되지 않은 상태
  • Runnable: 실행 중이거나 실행 준비가 된 상태
  • Blocked/Waiting: 다른 스레드에 의해 잠시 멈춰 있는 상태
  • Timed Waiting: 일정 시간 후에 다시 실행될 상태
  • Terminated: 스레드의 작업이 모두 끝난 상태

한 사람이 일어나서 출근 준비를 하는 것처럼, 스레드도 만들어진 후 실행 준비, 작업 중, 대기, 그리고 작업 종료의 과정을 거친다.

 

 

 

근데 문제가 발생할 수 있다. 멀티스레딩에서 여러 스레드가 동시에 같은 데이터를 수정하려 할 때 문제가 발생할 수 있다.

이를 경쟁 조건 (Race Condition) 이라고 하며, 이를 해결하기 위해 동기화(Synchronization) 를 사용한다.

 

예를 들어 콘서트 티켓을 예매할때 한 좌석을 동시에 두명이 예매하려고 할때 좌석을 누구에게 할당해야 할까?  이런 문제를 해결하려면, 한 사람이 작업을 끝낼 때까지 기다리도록 해야 한다.

 

 

synchronized 키워드 사용 예제

class Counter {
    var count: Int = 0

    // synchronized를 사용해 여러 스레드가 동시에 count를 수정하지 않도록 보호합니다.
    @Synchronized
    fun increment() {
        count++
    }
}

fun main() {
    val counter = Counter()
    val threads = mutableListOf<Thread>()

    // 10개의 스레드를 생성하여 동시에 increment()를 호출합니다.
    for (i in 1..10) {
        val thread = Thread {
            for (j in 1..1000) {
                counter.increment()
            }
        }
        threads.add(thread)
        thread.start()
    }

    // 모든 스레드가 끝날 때까지 대기합니다.
    threads.forEach { it.join() }

    // 10개의 스레드가 각각 1000번씩 increment했으므로, 최종 결과는 10000이어야 합니다.
    println("최종 count: ${counter.count}")  // 결과: 10000
}
  • @Synchronized 어노테이션을 사용해 increment() 메서드에 동시에 접근하는 것을 막는다.
  • 여러 스레드가 동시에 increment() 를 호출해도, 동기화 덕분에 안전하게 실행된다.   

 

 

내가 회사에서 일하면서 실제로 겪은 스레드 문제가 있다.

 

경쟁 조건과 데드락

  • 경쟁조건 : 여러 스레드가 동시에 데이터를 수정할 때 예상치 못한 결과가 발생하는것
  • 데드락(DeadLock) : 두 스레드가 서로 상대방이 가진 자원을 기다리면서 무한 대기에 빠지는 상황이다.

동기화 블록이나 Lock 객체를 사용해, 자원에 접근하는 순서를 잘 관리해야 한다. 

 

실제로 데드락에 빠지는 로직을 개발하여 정말..난리 난리가 났었던...일이...후..

 

 

스레드 풀 (Thread Pool) 

매번 새로운 스레드를 생성하는 대신, 미리 일정 개수의 스레드를 만들어 두고 재사용하는 방법. 스레드의 생성 비용을 줄이고, 시스템 자원을 효율적으로 사용할 수 있다.

import java.util.concurrent.Executors

fun main() {
    // 고정 크기의 스레드 풀 생성 (3개의 스레드)
    val executor = Executors.newFixedThreadPool(3)

    // 10개의 작업을 스레드 풀에 제출합니다.
    for (i in 1..10) {
        executor.submit {
            println("작업 $i 시작: ${Thread.currentThread().name}")
            Thread.sleep(1000)
            println("작업 $i 완료: ${Thread.currentThread().name}")
        }
    }

    // 스레드 풀 종료
    executor.shutdown()
}
  • Executors.newFixedThreadPool(3)를 통해 3개의 스레드로 이루어진 풀을 생성한다.
  • 여러 작업이 동시에 제출되지만, 동시에 최대 3개 작업만 실행되고 나머지는 대기한다.

사용자 관점에서 한번 생각해보자

 

대부분의 웹 애플리케이션에서는 사용자가 데이터를 수정하고 저장을 누르면, 스레드 풀(Thread Pool) 에서 미리 만들어진 스레드 중 하나가 해당 요청을 처리한다.

 

즉, 사용자가 콘텐츠를 수정하고 저장 버튼을 클릭하면

  1. 웹 서버는 이미 생성되어 대기 중인 스레드 풀에서 하나의 스레드를 할당한다
  2. 해당 스레드가 수정 작업 로직을 실행하고
  3. 작업이 완료되면 그 스레드는 스레드 풀로 돌아가 재사용된다.

이제 스레드에 대해 확실히 알게 된 것 같다.

 

 

 

'Back-End > Java' 카테고리의 다른 글

try-catch  (0) 2024.07.06
스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05

자바의 예외처리 방법인 try-catch에 대해서 정리해보자

 

다음의 코드를 보고 결과값을 생각해보자

public static void main(String[] args) throws Exception {
	
    for(int i = 0; i < 10; i++) {
    	System.out.println(i);
        
        if(i==5) {
        	throw new Exception();
        }   
    }
}

 

해당 코드를 실행하면 출력값은 어떻게 될까???

 

출력값 : 0 1 2 3 4 5 

 

5번 인덱스 출력 후  중단된다.

 

그럼 다음 코드의 출력값은 어떨까?

 

public static void main(String[] args) throws Exception {
	
    for(int i = 0; i < 10; i++) {
    	System.out.println(i);
        
        if(i==5) {
        	try {
            	throw new Exception();
            } catch (Exception e) {
            	e.printStackTrace();
            }
        	
        }   
    }
}

 

출력값 : 0 1 2 3 4 5 6 7 8 9

 

try-catch 내에서 예외가 발생하면 해당 예외는 catch 블록을 통해 예외가 처리된다.

따라서, 정상적으로 for loop가 작동하게 된다.

 

반면에, 맨위 코드의 경우 throw new Exception(); 을 통해 예외를 던지고 있다. 하지만 이것을 받아줄 catch 부분이 없기 때문에 종료되게 된다.

 

 

다중 try-catch

public static void main(String[] args) {

		try {
			System.out.println("외부 try 로직 수행.");
			try {
				System.out.println("내부 try 로직 수행");
				Exception e = new Exception();
				throw e;
			} catch (Exception e) {
				System.out.println("내부 try catch문 exeption : " + e);
				System.out.println("예외 던지기 한번 더");
				throw e;
			}finally {
				System.out.println("finally 구문 출력");
			}
		} catch (Exception e) {
			System.out.println("외부 try - catch exception : " + e);
		}
		System.out.println("종료");
	}

위 코드의 실행 순서를 생각해보자.

  1. '외부 try 로직 수행.' 출력
  2. '내부 try 로직 수행.' 출력
  3. '내부 try catch문 exception : java.lang.Exception' 출력
  4. '예외 던지기 한번 더' 출력
  5. 'finally 구문 출력' 출력
  6. '외부 try - catch exception : java.lang.Exception' 출력
  7. '종료' 출력

내부의 예외처리부분인 catch에서 강제로 Exception 을 한번 더 발생시켰을때 처리해줄 catch 부분이 외부 catch 부분이기 때문에

'외부 try - catch exception : java.lang.Exception' 이 출력되게 된다.

'Back-End > Java' 카테고리의 다른 글

할머니도 이해할 수 있는 자바 Thread  (0) 2025.05.01
스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05

사전적 의미로 한 가닥의 실이라는 뜻으로 한가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름이므로 한 프로세스 내 스레드가 2개라면 2개의 실행 흐름이 생긴다.

 

프로세스가 할당받은 자원을 이용하는 실행의 단위이며

  • 프로세스 내의 명령어 블록으로 시작점과 종료점을 가진다.
  • 실행중에 멈출 수 있으며 동시에 수행 가능하다.
  • 어떠한 프로그램 내에서 특히 프로세스 내에서 실행되는 흐름의 단위이다.

자바에서는 멀티 스레딩을 통해 하나의 프로세스 내에서 여러 스레드가 병렬로 실행될 수 있다.

 

스레드의 특징

  1. 공유 주소 공간 : 같은 프로세스 내의 스레드들은 동일한 메모리 공간을 공유한다. 이를 통해 스레드 간 데이터 공유와 통신이 매우 효율적이다.
  2. 독립적인 실행 흐름 : 각 스레드는 자신만의 실행 흐름을 가지며, 독립적으로 스케줄링되어 실행할 수 있다.
  3. 스레드 동기화 : 스레드들이 같은 자원을 동시에 접근할 때 발생할 수 있는 문제를 방지하기 위해 동기화 메커니즘이 필요하다 자바에서는 'synchronized' 키워드와 'java.util.concurrent' 패키지의 여러 클래스를 통해 동기화를 지원한다.
  4. 생성 및 관리 : 자바에서 스레드는 두 가지 방법으로 실행할 수 있다. 
    1. 'Tread' 클래스를 상속받아 새로운 클래스를 만들고 'run()' 메서드를 오버라이딩
    2. 'Runnable' 인터페이스를 구현하여 스레드 객체에 전달
// Thread 클래스를 상속받아 스레드 생성
class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running.");
    }
}

MyThread t1 = new MyThread();
t1.start();

// Runnable 인터페이스를 구현하여 스레드 생성
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread is running.");
    }
}

Thread t2 = new Thread(new MyRunnable());
t2.start();

 

스레드의 동작을 확인할 수 있는 예제

public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main end.");  // main 메서드 종료
    }
}

 

위의 예제는 총 10개의 스레드를 실행시키는 예제이다. 어떤 스레드인지 확인하기 위해 스레드마다 생성자에 순서를 부여한다. 그리고 시작과 종료를 출력하게 했고 시작과 종료 사이에 1초의 간격이 생기도록 Tread.slepp(1000) 을 작성하였다. 그리고 main 메서드 종료 시 'main.end'를 출력하도록 했다. 결과를 보자

0 thread start.
4 thread start.
6 thread start.
2 thread start.
main end.
3 thread start.
7 thread start.
8 thread start.
1 thread start.
9 thread start.
5 thread start.
0 thread end.
4 thread end.
2 thread end.
6 thread end.
7 thread end.
3 thread end.
8 thread end.
9 thread end.
1 thread end.
5 thread end.

 

결과는 출력 때마다 다르다. 0번 스레드부터 9번 스레드까지 순서대로 실행되지 않고, 그 순서가 일정하지 않은 것을 보면 스레드는 순서에 상관없이 동시에 실행된다는 사실을 알 수 있다. 더욱 놀라운 사실은 스레드가 종료되기 전 main 메서드가 종료되었다는 사실이다.

 

그렇다면 모든 스레드가 종료된 후에 main 메서드를 종료하고 싶은 경우에는 어떻게 해야 할까?

import java.util.ArrayList;

public class Sample extends Thread {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for(int i=0; i<10; i++) {
            Thread t = new Sample(i);
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join(); // t 쓰레드가 종료할 때까지 기다린다.
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}

 

생성된 스레드를 ArrayList 객체인 threads 에 담은 후 main 메서드가 종료되기 전에 threads 객체에 담긴 각각의 스레드에 join 메서드를 호출하여 스레드가 종료될때까지 대기하도록 했다. join 메서드는 스레드가 종료될 때까지 기다리게 하는 메서드이다.

 

0 thread start.
5 thread start.
2 thread start.
6 thread start.
9 thread start.
1 thread start.
7 thread start.
3 thread start.
8 thread start.
4 thread start.
0 thread end.
5 thread end.
2 thread end.
9 thread end.
6 thread end.
1 thread end.
7 thread end.
4 thread end.
8 thread end.
3 thread end.
main end.

 

이렇게 하면 'main end.' 문자열이 가장 마지막에 출력되는 것을 확인할 수 있다.

'Back-End > Java' 카테고리의 다른 글

할머니도 이해할 수 있는 자바 Thread  (0) 2025.05.01
try-catch  (0) 2024.07.06
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05

프로세스(Process)

운영체제로부터 자원을 할당받는 작업의 단위

사용자가 어플리케이션을 실행하면, 운영체제로부터 실행에 필요한 메모리를 할당받아 어플리케이션의 코드를 실행하는데 이것을 프로세스라고 부른다.

 

ex) Chrome 브라우저 2개 실행 = 두 개의 Chrome 프로세스가 생성 되었다.

 

자바에서 프로세스는 자바 가상 머신(JVM)이 운영체제에서 프로그램을 실행할때 생성된다. 프로세스는 독립된 실행 환경을 가지며, 다음과 같은 특징을 가진다.

  1. 독립된 주소 공간 : 프로세스는 자신의 메모리 공간을 독립적으로 가지고 있다. 다른 프로세스와 메모리 공간을 공유하지 않기 때문에 하나의 프로세스에서 발생한 오류가 다른 프로세스에 영향을 미치지 않는다.
  2. 자원 할당 : 프로세스는 CPU 시간, 메모리, 파일 핸들 등의 자원을 할당받는다.
  3. 프로세스 간 통신 : 서로 다른 프로세스는 기본적으로 독립적이기 때문에, 프로레스 간의 데이터 교환은 인터프로세스 커뮤니케이션(IPC) 기법을 통해 이루어져야 한다. IPC 방법으로는 소켓, 파일, 공유 메모리, 메시지 큐 등이 있다.

자바에서 새로운 프로세스를 생성하려면 

  1. Runtime.getRuntime().exec() 메서드를 사용하거나
  2. ProcessBuilder 클래스를 사용할 수 있습니다.

 

Runtime 클래스의 exec 메서드를  사용

import java.io.*;

public class ProcessExample {
    public static void main(String[] args) {
        try {
            // 새로운 프로세스를 생성하여 명령어 실행
            Process process = Runtime.getRuntime().exec("notepad.exe");

            // 프로세스의 출력 스트림 읽기 (예시에서는 필요 없지만 다른 명령어 실행 시 유용할 수 있음)
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }

            // 프로세스 종료 대기
            int exitCode = process.waitFor();
            System.out.println("Process exited with code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

 

ProcessBuilder 클래스를 사용하는 예시

mport java.io.*;

public class ProcessBuilderExample {
    public static void main(String[] args) {
        // ProcessBuilder 객체 생성
        ProcessBuilder processBuilder = new ProcessBuilder("ping", "-c", "4", "google.com");

        // 프로세스 출력과 오류를 동일한 스트림으로 병합
        processBuilder.redirectErrorStream(true);

        try {
            // 새로운 프로세스 시작
            Process process = processBuilder.start();

            // 프로세스의 출력 스트림 읽기
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }

            // 프로세스 종료 대기
            int exitCode = process.waitFor();
            System.out.println("Process exited with code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

 

프로세스 간 통신 예시

자바에서는 소켓을 사용하여 프로세스 간 통신을 구현할 수 있다. 간단한 서버-클라이언트 예제를 통해 프로세스 간 통신을 알아보자

 

서버 예제

import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) {
        // 서버 소켓을 생성하여 포트 5000에서 클라이언트 연결 대기
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("Server started. Waiting for a client...");

            // 클라이언트 연결 수락
            try (Socket clientSocket = serverSocket.accept();
                 // 클라이언트와 통신을 위한 출력 스트림 생성
                 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                 // 클라이언트와 통신을 위한 입력 스트림 생성
                 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {

                System.out.println("Client connected.");

                String inputLine;
                // 클라이언트로부터 데이터 수신 및 처리
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("Received: " + inputLine);
                    // 클라이언트로 데이터 전송
                    out.println("Echo: " + inputLine);
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

클라이언트 예제

import java.io.*;
import java.net.*;

public class Client {
    public static void main(String[] args) {
        // 서버에 연결
        try (Socket socket = new Socket("localhost", 5000);
             // 서버로 데이터 전송을 위한 출력 스트림 생성
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             // 서버로부터 데이터 수신을 위한 입력 스트림 생성
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             // 사용자 입력을 위한 표준 입력 스트림 생성
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {

            String userInput;
            // 사용자 입력을 읽어 서버로 전송하고, 서버의 응답을 출력
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                System.out.println("Server response: " + in.readLine());
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

이 예시는 서버가 클라이언트의 메시지를 받아 그대로 돌려주는 간단한 코드이다. 

'Back-End > Java' 카테고리의 다른 글

try-catch  (0) 2024.07.06
스레드  (0) 2024.07.05
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05
String, StringBuilder, StringBuffer 의 차이점  (0) 2023.10.26

Java HTTP 통신에 대해서 알아보자

 

java.net 패키지내에 있는 HttpUrlConnection 클래스와 URL 클래스를 활용하여 HTTP 통신을  할 수 있다.

 

1. URL 클래스 

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URL.html

 

URL (Java SE 11 & JDK 11 )

Creates a URL object from the specified protocol, host, port number, and file. host can be expressed as a host name or a literal IP address. If IPv6 literal address is used, it should be enclosed in square brackets ('[' and ']'), as specified by RFC 2732;

docs.oracle.com

 

URL 클래스는 자원을 요청할 주소를 나타내는 클래스이다. URL 클래스를 살펴보면 Serializable 인터페이스를 구현하고 있어, 직렬화를 위한 조건을 갖추고 있다.

 

public final class URL implements java.io.Serializable {}

 

더보기

직렬화 (serialize)란 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용 할 수 있도록 바이트 스트림 (Stream of bytes) 형태로 연속적인(serial) 데이터로 변환하는 포맷 변환 기술을 말한다. 반대인 역직렬화는(Deserialize) 는 바이트로 변환된 데이터를 원래대로 자바 시스템의 Ojbect 또는 Data로 변환하는 기술이다.

 

* 바이트 스트림 이란?

 스트림은 클라이언트나 서버 간에 출발지 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다. 자바는 스트림의 기본 단위를 바이트로  두고 있기 때문에, 네트워크, 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림으로 변환하여 처리한다.

 

그런데 JSON을 사용하지 않고 왜 직렬화하여 사용할까?

 첫번째는 직렬화는 자바의 고유 기술인 만큼 당연히 자바 시스템에서 개발에 최적화되어 있다.

두번째는, 자바의 광활한 레퍼런스 타입에 대해 제약 없이 외부에 내보낼 수 있다는 것이다.

 

 

 1) URL 객체 생성하기

 URL 클래스의 생성자는 다양한 형태로 선언되어 있다. 그 중에 가장 많이 이용되는 생성자는 URL을 문자열 형태로 나타낸 객체의 레퍼런스를 인자로 전달받는 생성자이다.

 

public URL(Stirng spec) throws MalformedURLException {,,,}

 

예를들어 https://www.naver.com 의 URL을 나타내는 URL 객체를 생성하고 싶다면 아래와 같이 작성하면 된다.

 

import java.net.URL;
...

URL url = new URL("https://wwww.naver.com");

 

공식 Java API 문서를 보면,  생성자를 비롯하여 모든 URL 클래스 객체의 생성자는 MalformedURLException 예외를 throws 하여 예외처리에 대한 책임을 전가하고 있다. MalformedURLException은 생성자의 인자로 받은 URL 문자열이 null 이거나 프로토콜을 알 수 없을 때 등의 상황에 발생한다. 따라서 URL 객체를 생성한 클래스에서 그 예외를 처리해주어야 한다.

 

try {
	URL url = new URL("https://www.naver.com");
} catch (MalformedURLException e) {
	e.printStackTrace();
}

 

 

2) 연결 객체 얻기

URL 클래스에 선언된 openConnection() 메소드는 URL 객체에 대한 연결을 담당하는 URLConnection 객체를 반환한다.

 

위에서 생성한 URL 객체에 대한 연결 객체를 얻으려면 아래와 같이 하면된다.

 

URLConnection connection = url.openConnection();

 

 

 

2. HttpURLConnection 클래스

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html

 

HttpURLConnection (Java SE 11 & JDK 11 )

Returns the error stream if the connection failed but the server sent useful data nonetheless. The typical example is when an HTTP server responds with a 404, which will cause a FileNotFoundException to be thrown in connect, but the server sent an HTML hel

docs.oracle.com

 

HttpURLConnection 클래스는 HTTP 프로토콜 통신을 위한 클래스이다. 각각의 객체들은 하나의 요청을 위해 사용된다.

HttpURLConnection 클래스를 살펴보면 URLConnection 클래스를 확장한(상속받은) 추상클래스임을 알 수 있다.

 

1) HttpURLConnection 객체 생성

위에서 URL 객체의 openConnection() 메소드를 통해 URLConnection 객체를 얻을 수 있었다. HttpURLConnection 객체는 URLConnection 객체를 확장하고(상속받고)있기 때문에 Type Casting 을 통해 HttpURLConnection객체를 쉽게 얻을 수 있다.

 

URL에 연결하는 HTTP 연결 객체를 생성하고 싶다면 다음과 같이 코드를 작성하면 된다.

 

import java.net.URL;
import java.net.HttpURLConnection;
...

URL url = new URL("https://www.naver.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

 

 

2) 요청 메소드 설정

HTTP 요청을 위해서는 요청 메소드를 설정해야 한다. setRequestMethod() 메소드는 요청 메소드를 문자열 파라미터로 받아서 유요한 요청 메소드면 method 멤버 변수에 요청 메소드를 저장하고, 아니면 ProtocolException 예외를 발생시킨다. 처리 가능한 요청 메소드로는 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE 가 있다.

public void setRequestMethod(String method) throws ProtocolException {
        if (connected) {
            throw new ProtocolException("Can't reset method: already connected");
        }
        // This restriction will prevent people from using this class to
        // experiment w/ new HTTP methods using java.  But it should
        // be placed for security - the request String could be
        // arbitrarily long.

        for (int i = 0; i < methods.length; i++) {
            if (methods[i].equals(method)) {
                if (method.equals("TRACE")) {
                    SecurityManager s = System.getSecurityManager();
                    if (s != null) {
                        s.checkPermission(new NetPermission("allowHttpTrace"));
                    }
                }
                this.method = method;
                return;
            }
        }
        throw new ProtocolException("Invalid HTTP method: " + method);
    }

 

method 멤버변수는 기본으로 "GET"으로 초기화되어있다. 따라서 setRequestMethod()를 통해 요청 메소드를 설정하지 않으면 GET 요청을 보내게 된다.

 

위에서 생성한 HttpURLConnection 객체로 POST 요청을 하고 싶다면 다음과 코드를 작성하면 된다.

connection.setReuqestMethod("POST");

 

 

3) 요청 헤더 설정

HttpURLConnection 클래스가 확장(상속)하는 URLConnection 클래스에 정의된 setRequestProperty() 메소드로 요청 헤더를 설정할 수 있다.

 

public void setRequestProperty(String key, String value) {
        checkConnected();
        if (key == null)
            throw new NullPointerException ("key is null");

        if (requests == null)
            requests = new MessageHeader();

        requests.set(key, value);
}

 

setRequestProperty() 메소드는 String 타입의 key, value 파라미터를 받는다. 각 파라미터를 통해 요청 헤더의 이름과 값을 설정할 수 있다. 만약 key값이 null 이라면 NullPointerException 예외를 발생시킨다.

 

Content-Type 및 HTTP 요청을 하는 사용자의 애플리케이션 타입, 운영 체제, 소프트웨어 벤더 또는 소프트웨어 버전 등을 식별할 수 있는 User-Agent 헤더를 설정하고 싶다면 다음과 같이 코드를 작성하면 된다.

 

private static final String USER_AGENT = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0";

...

connection.setRequestProperty("User-Agent", USER_AGENT);
connection.setReuqestProperty("Content-Type", "application/json; charset=utf-8");

 

 

4) POST 요청시 데이터 넘겨주기

POST 요청을 할 때에는 OutputStream 객체로 데이터를 전송한다. setDoOutPut() 메소드를 통해 OutputStream 객체로 전송할 데이터가 있다는 옵션을 설정해야 한다. checkConnected() 메소드에서는 연결 객체가 연결되어있는지 확인하고, 이미 연결되어있다면 IllegalStateException 예외를 발생시킨다.

 

public void setDoOutput(boolean dooutput) {
    checkConnected();
    doOutput = dooutput;
}

 

위처럼 setDoOutput() 메소드는 boolean 타입의 dooutput 파라미터를 받으 doOutput 멤버 변수에 저장한다. doOutput 변수가 true 이면 OutputStream 으로 데이터를 전송한다는 뜻이고, false이면 하지 않는다는 뜻인데, 기본적으로 false로 초기화되어있기 때문에 POST로 데이터를 전송하려면 꼭 옵션을 설정해줘야 한다.

 

getOutputStream() 메소드를 통해 연결에 사용할 OutputStream 객체를 얻을 수 있다. 프로토콜이 출력을 지원하지 않는다면 UnknownServiceException 예외를 발생시킨다.

 

public OutputStream getOutputStream() throws IOException {
     throw new UnknownServiceException("protocol doesn't support output");
}

 

전송할 데이터가 문자열일 경우는 OutputStream 클래스를 확장하는 DataOutputStream 클래스의 writebytes() 메소드를 활용하여 쉽게 데이터를 설정할 수 있습니다. DataOutputStream 클래스는 생성자에 OutptStream 객체를 전달하여 생성할 수 있습니다. 따라서 위에서 getOutputSteam() 메소드를 통해 얻은 객체를 바로 넣어줄 수 있다.

 

5) 응답 코드 얻기

getResponseCode() 메소드를 통해 응답 코드를 얻을 수 있습니다. 정상적인 응답일 경우 200이 반환됩니다.

 

 

6) 응답 데이터 얻기

getInputStream() 메소드를 통해 응답 데이터를 읽을 수 있는 InputStream객체를 얻을 수 있습니다. 응답을 문자열 타입으로 얻기 위해 BufferedReader 객체를 사용할 수 있습니다.

 

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuffer stringBuffer = new StringBuffer();
String inputLine;

while ((inputLine = bufferedReader.readLine()) != null)  {
     stringBuffer.append(inputLine);
}
bufferedReader.close();

String response = stringBuffer.toString();

'Back-End > Java' 카테고리의 다른 글

스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Exception과 Transaction  (0) 2024.02.05
String, StringBuilder, StringBuffer 의 차이점  (0) 2023.10.26
스프링이란 무엇일까?  (0) 2023.09.16

Exception 과 Error

Exception 과 Error 는 한 마디로 개발자가 대응할 수 있냐 없냐로 구분할 수 있다.

 

- Exception : 프로그램을 실행 중 발생하는 예외 상황을 나타낸다. 예외는 프로그램이 실행되는 동안 발생할 수 있는 일반적인 에러를 포함한다. 예를 들어, 파일을 찾을 수 없는 경우, 네트워크 연결이 끊어진 경우, 잘못된 사용자 입력 등이 있다.

 

- Error : 프로그램이 더 이상 수행될 수 없는 심각한 예외 상황을 나타낸다. 일반적으로 Error는 시스템 수준의 문제를 나타내며, 프로그래머가 직접적으로 처리하기 어렵거나 불가능한 상황이다. 프로그램이 더 이상 복구할 수 없는 상황에서 발생하는데, 가장 흔한 예로는 OutOfMemoryError 가 있다.

 

자바에서는 이러한 예외를 처리하기 위해 'try', 'catch', 'finally', 'thorw', 'thorws' 등의 예외 처리 구문을 제공한다.

 

try {
	// 예외가 발생할 수 있는 코드 작성 
} catch {
	// 예외를 처리하는 코드 작성
} finally {
	// 항상 실행되는 코드 작성 (예외 발생 여부에 상관없이)
}

 

 

 

추가로 이러한 오류가 발생하는 시점에 따라 분류를 할 수도 있는데

   1. 컴파일 에러 (compile-time error): 컴파일시에 발생하는 에러

   2. 런타임 에러 (runtime error): 프로그램 실행시에 발생하는 에러

   3. 논리적 에러 (logical error): 실행은 되지만 의도와 다르게 동작하는 것

로 나뉜다. 

 

* 논리 에러 (logical error) 는 버그라고 생각하면 된다. 프로그램이 실행되고 작동하는데는 아무런 문제가 없는 오류지만, 결과가 예상과 달라 개발자가 의도한 작업을 수행하지 못하게 되어 서비스 이용에 지장이 생길 수 있다. 

이러한 논리 에러는 프로그램이 멀쩡하게 돌아가기 떄문에 따로 에러 메시지는 발생하지 않는다. 따라서 논리 에러를 피하기 위해서는 프로그램의 전반적인 코드와 알고리즘을 체크하고 테스트를 진행해야 한다.

 

* 컴파일 에러 (compile-time error) : 컴파일 에러는 컴파일 단계에서 오류를 발견하면 컴파일러가 에러 메시지를 출력해주는 것을 말한다. 대표적인 컴파일 에러 발생의 원인은 '문법 구문 오류 (syntax error)'를 들 수 있다.

 

* 런타임 에러 (runtime error) : 프로그램 실행 중에 에러가 발생하여 잘못된 결과를 얻는 경우 런타임 에러가 발생할 수 있다.

 

 

자바의 오류 클래스 계층 구조를 확인해보자

Exception 계층 구조

 

 

자바에서 다루는 모든 예외 처리는 Exception 클래스에서 처리한다. Exception 클래스는 RunTimeException 과 CompileException 으로 구분 할 수 있다.

 

그렇다면 RuntimeException 클래스의 종류에는 어떠한 것들이 있을까?

 

런타임 예외의 종류

 

 

@Transaction 어노테이션

@Transaction 어노테이션은 saveDataInTransaction 메서드에 적용되어있다. 이 메서드가 실행될때 스프링은 트랜잭션을 시작하고, 메서드가 완료되면 트랜잭션을 커밋한다. 만약 메서드 수행 중 예외가 발생하면 트랜잭션은 롤백된다. 

 

Spring @Transactional 선언 시 기본 속성은 아래와 같다.

@Transactional
//@Transactional(rollbackFro = {RuntimeException.class, Error.class}) 기본 속성
public void doSave(User userInfo) {
	 userInfoSaveDao.save(user);
}

 

RuntimeException 과 Error 가 발생했을 경우 기본적으로 rollback이 된다. 해당 2가지 경우가 아니면 rollback 되지 않는다. (컴파일 예외는 rollback되지 않는다)

 

하지만 트랜잭션을 rollback 하는 다른 방법들이 존재한다.

 

1. @Transactional 의 옵션 값을 변경

@Transactional(rollbackFor = {Exception.class})
public void doSave(User userInfo) {
	 userInfoSaveDao.save(user);
}

위와 같이 변경하면 모든 예외에 대해서 rollback을 진행하게 된다.

 

2. RuntimeException

@Transactional(rollbackFor = {RuntimeException.class, Error.class})
public void doSave(User userInfo) {
	 userInfoSaveDao.save(user);
}

 

3. 수동으로 하는 rollback

@Autowired
private DataSourceTransactionManager txm;


@Transactional(rollbackFor = {Exception.class})
public void doSave(User userInfo) {
	
    TransactionStatus tx = CommonUtils.getTransactionStatus(txm);

	try {
		 userInfoSaveDao.save(user);    
    } catch(Exception e) {
    	tx.rollback();
    }

}

'Back-End > Java' 카테고리의 다른 글

스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
String, StringBuilder, StringBuffer 의 차이점  (0) 2023.10.26
스프링이란 무엇일까?  (0) 2023.09.16

자바에서는 대표적으로 문자열을 처리하는 클래스로 String, StringBuilder, StringBuffer 라는 3가지 클래스를 제공한다.

3가지로 구분되는 이유는 무엇이며 무엇이 다른지 정리해보자.

 

가장 먼저 String 과 다른 클래스의 차이점은 String 은 immutable(불변), 다른 두 클래스는 mutable(가변)의 차이점이다.

 

String 자료형만으로도 + 연산이나 concat() 메소드로 문자열을 이어붙일 수 있다. 하지만 덧셈(+) 연산자를 이용해

String 인스턴스의 문자열을 결합하면, 내용이 합쳐진 새로운 String 인스턴스를 생성하게 되어 문자열을 많이 결합하면 할수록

공간의 낭비가 생기고 속도 또한 매우 느려지게 된다. 왜냐하면 초기 공간과 다른 값에 대한 연산에서 많은 시간과 자원을 사용하게 되기

때문이다.

 

String 으로 할당했을때 메모리 사용을 살펴보자

 

 

그래서 자바에서는 이러한 문자열 연산의 낭비를 막기 위해 문자열 전용 자료형을 제공해주는데 StringBuffer 이다.

StringBuffer 클래스는 내부적으로 Buffer 라고 하는 독립적인 공간을 가지게 되어, 문자열을 바로 추가할 수 있어 공간의

낭비도 없으며 문자열 연산 속도도 매우 빠르다는 특징이 있다.

 

StringBuffer sb = new StringBuffer(); // StringBuffer 객체 생성

sb.append("Hello");
sb.append(" "):
sb.append("Java");
String result = sb.toString();

System.out.println(result); // Hello Java

StringBuilder는 StringBuffer와 거의 비슷한 자료형인데 둘의 차이점은 

StringBuffer 는 멀티 스레드 환경에서 안전하다는 장점이 있고, StringBuilder 는 문자열 파싱 성능이 가장 우수하다는 장점이 있다.

 

정리하자면 

  • String은 짧은 문자열을 더할 경우 사용한다.
  • StringBuffer는 스레드에 안전한 프로그램을 개발할때, 개발 중인 시스템이 스레드에 안전한지 모를 경우 사용한다.
  • StringBuilder 는 스레드의 안전 여부가 상관 없는 프로그램을 개발할 때 사용하면 좋다.

 

StringBuffer 의 버퍼(데이터 공간)의 크기의 기본값은 16개의 문자열을 저장할 수 있는 크기이며, 생성자를 통해 그 크기를 

별도로 설정할 수도 있다. 

만약 문자열 연산중 할당된 버퍼의 크기를 넘겨도 상관없다. 자동으로 버퍼를 증가시켜 주기 때문이다.

 

메서드 설명
StringBuffer() 버퍼의 길이를 지정하지 않으면 크기가 16인 버퍼를 생성한다.
StringBuffer(int length) length 길이를 가진 StringBuffer 클래스의 인스턴스(buffer)를 생성
StringBuffer(String str) 지정한 문자열 (str)의 길이보다 16 만큼 더 큰 버퍼를 생성
StringBuffer append() 매개변수로 입력된 값을 문자열로 변환하여 StringBuffer 인스턴스가 저장하고 있는 
문자열의 뒤에 덧붙임
int capacity() StringBuffer 인스턴스의 버퍼크기 반환
int length() StringBuffer 인스턴스에 저장된 문자열의 길이 반환
char charAt(int index) 지정된 위치(index) 에 있는 문자를 반환
StringBuffer delete(int short, int end) 시작위치(start) 부터 끝 위치(end) 사이에 있는 문자를 제거, 단 end 위치의 문자는 제외
StringBuffer deleteCharAt(int index) 지정된 위치(index)의 문자를 제거
StringBuffer insert(int pos, boolean b)
StringBuffer insert(int pos, String str) 

등등
두 번째 매개변수로 받은 값을 문자열로 변황하여 지정된 위치(pos)에 추가 
StringBuffer replace(int start, int end, String str) 지정된 범위 (start ~ end) 의 문자들을 주어진 문자열로 변경
String toString() StringBuffer 인스턴스의 문자열을 String 으로 변환
void setLength(int newLength) 지정된 길이로 문자열의 길이를 변경 
길이를 늘리는 경우에는 나머지 빈공간들을 널문자(\u000)로 채운다.
String subString(int start) 지정된 밤위 내의 문자열을 String 으로 뽑아서 반환

StringBuffer 클래스의 메서드와 StringBuilder 클래스 메서드 사용법은 동일하다.

 

StringBuffer / StringBuilder 의 메모리공간을 살펴보자

따라서 값이 변경될 때마다 새롭게 객체를 만드는 String 보다는 문자열의 추가,수정,삭제가 빈번하게 발생한다면

StringBuffer / StringBuilder 를 사용하는것이 좋다.

 

간단하지만 이해하기 쉬운 예제로 for문을 돌면서 *을 추가하는 예제를 들 수 있다.

String str = "*";

for (int i = 1; i < 10; i++) {
	str += "*";
}
StringBuffer sb = new StringBuffer("*");

for(int i = 1; i < 10; i++) {
	sb.append("*");
}

String 객체일 경우 매번 * 문자열이 업데이트 될때마다 계속해서 메모리 블럭이 추가되게 되고, 일회용으로 사용된 이 매모리들은 후에 

Garbage Collector (GC)의 제거 대상이 되어 빈번하게 Minor GC를 일으켜 Full GC(Major GC)를 일으킬 수 있는 원인이 된다.

'Back-End > Java' 카테고리의 다른 글

스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05
스프링이란 무엇일까?  (0) 2023.09.16
  • 스프링은 프레임워크다.
  • 스프링은 오픈소스다.
  • 스프링은 IOC 컨테이너를 가진다.
  • 스프링은 DI를 지원한다.
  • 스프링은 엄청나게 많은 필터를 가지고 있다.
  • 스프링은 MessageContertor를  가지고 있다. 기본값은 현재 Json이다.
  • 스프링은 BufferedReader 와 BufferedWriter를 쉽게 사용가능하다.\

 

스프링은 프레임워크다

framework 를 살펴보면 frame : 틀 + work : 동작하다 이다. 

풀이를 해보면 어떤 틀에서 동작하는 것이다. 틀을 제공해주고 여기에 맞춰서 개발을 하면 누구나 좋은 프로그램을 만들 수 있기 때문에 사용한다.

 

 

IOC(Inversion Of Controll)

뜻을 해석해 보면 역전의 제어이다 즉 주도권이 스프링에게 있다는 것이다.

 

우선 class, object, instance 에 대해 알아야 한다.

  • class : 설계도
  • object : 실체화가 가능한 어떤 것
  • instance : 실체과 된 것

만약 가구가 있다고 해보자 가구는 추상적이다. 가구에 해당하는 의자, 침대, 책상이 있다고 하면 의자, 침대, 책상은 object에 해당한다. 이 object들이 실체화가 되면 그게 instance가 되는 것이다.

 

그렇다면 IOC 는 무엇일까? 

만약 오브젝트를 만들어 본다고 하면 

 

의자 chair = new 의자();

위의 chair 는 힙 메모리에 저장되어 사용 가능하게 된다. IOC는 이러한 수많은 object 들을 읽어서 직접 사용가능하게 해주는 것이다.

 

 

DI(Dependencies Injection)

위 단어를 해석해 보면 의존성 주입이라는 뜻이다. 

스프링이 내가 만든 class 들을 관리하는데 내가 원하는 다른 곳에서 사용(공유)할 수 있게 해주는 것이 DI이다.

위에 있는 IOC와 연관이 되어 있다.

 

'Back-End > Java' 카테고리의 다른 글

스레드  (0) 2024.07.05
프로세스  (0) 2024.07.04
Java HTTP 통신  (1) 2024.06.12
Exception과 Transaction  (0) 2024.02.05
String, StringBuilder, StringBuffer 의 차이점  (0) 2023.10.26

+ Recent posts