Now Loading ...
-
💾 [CS] JWT(JSON Web Token) 토큰이란 무엇일까요?
💾 [CS] JWT(JSON Web Token) 토큰이란 무엇일까요?
JWT(JSON Web Token) 토큰은 사용자 인증과 정보를 안전하게 전달하기 위한 JSON 기반의 토큰입니다.
주로 웹 애플리케이션에서 사용자 인증, 권한 부여를 위해 사용됩니다.
JWT(JSON Web Token)는 디지털 서명을 통해 변조 방지가 가능하며, 이를 통해 클라이언트와 서버 간의 데이터 전송이 안전하게 이루어집니다.
1️⃣ JWT의 구조.
JWT(JSON Web Token)는 세 가지 부분으로 구성됩니다.
헤더(Header)
페이로드(Payload)
서명(Signature)
1️⃣ 헤더(Header)
토큰의 타입(JWT-JSON Web Token)과 해싱 알고리즘 정보(예: HMAC, SHA256)를 포함합니다.
예시: {"alg": "HS256", "typ": "JWT"}
🤔 JWT의 헤더에서 토근의 타입을 포함하는 이유는 무엇일까?
JWT(JSON Web Token)의 헤더(Header)에 토큰의 타입을 포함하는 이유는 수신자가 토큰을 올바르게 인식하고 처리할 수 있도록 하기 위해서 입니다.
🤔 토큰의 타입(Token Type)이란 무엇일까?
토큰이 사용되는 방식이나 형태를 구분하는 정보입니다.
이 정보는 주로 토큰이 어떤 인증 또는 정송 방식으로 사용되는지를 명시하며, 수신 측에서 해당 토큰을 올바르게 해석하고 처리하는 데 중요한 역할을 합니다.
🤔 JWT의 헤더에서 해싱 알고리즘 정보를 포함하는 이유는 무엇일까?
JWT(JSON Web Token)의 헤더(Header)에 해싱 알고리즘 정보를 포함하는 이유는 토큰의 무결성을 검증하기 위해 사용된 알고리즘이 무엇인지 수신 측에서 정확히 알 수 있도록 하기 위해서입니다.
🤔 해시값(Hash Value), 또는 해시 코드(Hash Code)는 무엇일까?
해시값(Hash Value), 또는 해시 코드(Hash Code)는 임의의 입력 데이터를 고정된 길이의 문자열이나 숫자로 변환한 값입니다.
해시값(Hash Value)은 해싱 알고리즘에 의해 생성되며, 입력 데이터가 같으면 항상 같은 해시값(Hash Value)을 생성하지만, 입력 데이터가 달라지면 완전히 다른 해시값(Hash Value)이 생성됩니다.
🤔 해싱 알고리즘(Hashing Algorithm)이란 무엇일까?
임의의 길이를 가진 데이터를 고정된 길이의 해시값(Hash Value), 또는 해시 코드(Hash Code)으로 변환하는 함수입니다.
해싱 알고리즘(Hashing Algorithm)은 데이터의 무결성 검증, 비밀번호 저장, 데이터 검색 등의 용도로 널리 사용됩니다.
해시 함수는 일반적으로 빠르게 계산할 수 있으며, 입력 데이터가 아주 약간만 바뀌어도 완전히 다른 해시값(Hash Value)을 생성하는 특징이 있습니다.
🤔 해싱 알고리즘 정보란 무엇일까요?
해싱 알고리즘(Hashing Algorithm)의 종류와 방식을 나타내는 정보로, 주로 데이터의 무결성을 검증하거나 암호화된 형태로 저장하기 위해 사용됩니다.
해싱은 데이터가 변경되지 않았음을 확인하는 데 유용하며, 특히 비밀번호 저장, 디지털 서명, 데이터 무결성 등 다양한 보안 분야에서 필수적으로 사용됩니다.
2️⃣ 페이로드(Payload)
토큰의 실제 정보가 포함된 부분으로, 사용자 정보와 같은 클레임(Claim)을 담고 있습니다.
🤔 토큰의 실제 정보란 무엇일까요?
토큰의 실제 정보는 토큰에 포함된 데이터의 핵심적인 내용으로, 주로 사용자에 대한 정보나 인증, 권한에 관련된 정보를 담고 있습니다.
이 정보는 JWT(JSON Web Token) 토큰의 페이로드(Payload) 부분에 저장되며, 클레임(Claim)이라고도 불립니다.
토큰의 실제 정보는 인증이 필요한 시스템에서 서버가 해당 사용자의 신원을 확인하거나 특정 권한을 부여할 때 사용됩니다.
🤔 클레임(Claim)이란 무엇일까요?
JWT(JSON Web Token)의 페이로드(Payload) 부분에 담기는 정보로, 사용자 또는 토큰과 관련된 속성이나 데이터를 나타냅니다.
클레임(Claim)은 서버가 사용자에 대한 신원, 권한, 토큰의 유효 기간 등을 확인 할 수 있게 하는 역할을 합니다.
JWT(JSON Web Token)의 핵심 요소로, 클레임(Claim)을 통해 인증과 권한 부여에 필요한 다양한 정보를 포함할 수 있습니다.
🤔 페이로드(Payload)가 토큰의 실제 정보를 포함하는 이유는 무엇일까요?
토큰을 통해 인증과 권한 부여에 필요한 사용자 정보와 기타 데이터를 효율적으로 전달하기 위해서입니다.
JWT(JSON Web Token)는 주로 웹 애플리케이션에서 서버와 클라이언트 간에 정보를 안전하게 전송하는 방식으로 사용되므로, 필요한 정보를 페이로드(Payload)에 포함하여 서버가 추가 요청 없이 사용자 상태를 파악할 수 있게 해줍니다.
클레임(Claim)은 일반적으로 사용자 식별자(id), 사용자 권한(role), 토큰의 유효 시간(expiration) 등을 포함합니다.
예시: {"sub": "1234567890", "name": "Kobe", "admin": true}
🤔 클레임(Claim)에서의 사용자 식별자(id)란 무엇일까요?
사용자 식별자(id)는 JWT(JSON Web Token) 토큰 내에서 사용자를 고유하게 식별할 수 있는 값을 의미합니다.
이 식별자는 사용자를 구별하는 주요 정보로 사용되며, 서버가 특정 사용자를 인식하고 인증, 권한 부여를 수행할 때 기준이 되는 정보입니다.
🤔 클레임(Claim)에서의 사용자 권한(role)이란 무엇일까요?
사용자가 시스템에서 어떤 역할을 가지고 있는지를 나타내는 정보입니다.
이는 JWT(JSON Web Token) 토큰의 클레임(Claim) 필드에 포함되어 사용자에게 부여된 권한 수준을 명시하며, 서버는 이 권한 정보를 기반으로 사용자에게 허용된 작업이나 접근 권한을 결정합니다.
🤔 클레임(Claim)에서의 토큰의 유효 시간(expiration)이란 무엇일까요?
토큰의 유효 시간(expiration)은 JWT(JSON Web Token) 토큰이 유효한 기간을 설정하는 속성으로, 토큰의 만료 시점을 나타냅니다.
이 속성은 JWT(JSON Web Token)의 exp 클레임(Claim)에 포함되며, 이 값이 지나면 토큰은 더 이상 유효하지 않다고 간주되어 인증과 권한 부여 요청 시 거부됩니다.
🤔 클레임(Claim)에서 사용자 식별자(id), 사용자 권한(role), 토큰의 유효 시간(expiration) 등을 포함하는 이유는 무엇일까요?
서버가 사용자 인증 및 권한 관리를 효율적이고 안전하게 수행할 수 있도록 필요한 정보를 한 곳에 모아 두기 위함입니다.
이러한 정보를 통해 서버는 별도의 추가 작업 없이 사용자의 신원과 권한, 토큰의 유효성을 검증할 수 있으며, 이를 통해 효율적이고 확장 가능한 인증 시스템을 구축할 수 있습니다.
3️⃣ 서명(Signature)
토큰의 무결성을 보장하기 위한 서명으로, 헤더(Header)와 페이로드(Payload)를 합친 후 비밀 키로 해싱(Hashing)하여 생성됩니다.
🤔 토큰의 무결성(Token Integrity)
토큰의 내용이 발급된 이후 변경되지 않았음을 보장하는 것을 의미합니다.
무결성이 유지된 토큰은 발급 시점의 정보를 신뢰할 수 있는 상태로 유지하며, 그 내용이 중간에 조작되지 않았음을 확신할 수 있습니다.
무결성은 토큰의 안전한 인증과 권한 관리를 위해 필수적인 요소입니다.
🤔 비밀 키(Secret Key)
데이터를 암호화하거나 인증할 때 사용하는 기밀 정보로, 오직 송신자와 수신자만 알고 있어야 하는 키입니다.
비밀 키는 대칭 암호화와 HMAC 해싱 알고리즘에서 주로 사용되며, 이를 통해 데이터의 무결성을 보장하고 전송 중 데이터의 안전성을 유지할 수 있습니다.
서명은 클라이언트가 페이로드(Payload)의 내용을 변경하지 못하도록 보장하는 역할을 합니다.
예시: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT는 위의 세 가지를 합쳐서 하나의 문자열로 만들고, 각 부분을 .으로 구분합니다.
2️⃣ JWT의 예시.
JWT(JSON Web Token)는 다음과 같이 세 부분이 합쳐진 형태의 문자열입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
이 문자열은 헤더(header), 페이로드(payload), 서명(signature)이 결합된 것이며, 각 부분은 Base64URL로 인코딩되어 있습니다.
🤔 Base64URL이란 무엇일까요?
Base64URL은 Base64 인코딩 방식을 URL 및 파일 시스템에서 안전하게 사용할 수 있도록 변형한 인코딩 방식입니다.
일반적인 Base64 인코딩은 문자열을 바이너리 데이터로부터 ASCII 텍스트로 변환하지만, 변환된 결과에는 URL에서 특별한 의미를 가지는 문자(+, /, =)가 포함될 수 있어 URL 인코딩을 추가로 해야 하는 불편함이 있습니다.
Base64URL은 이 문제를 해결하여, Base64 인코딩을 URL과 파일 경로에서 사용할 수 있도록 안전하게 변경한 방식입니다.
🤔 바이너리 데이터(Binary Data)
컴퓨터가 2진수(0과 1)로 표현하는 데이터를 의미하며, 텍스트 데이터와 달리 사람이 직접 읽거나 해석하기 어려운 데이터 형식입니다.
컴퓨터 시스템에서 모든 데이터는 궁극적으로 0과 1의 이진수 형태로 저장되고 처리되기 때문에, 이미지, 오디오, 비디오, 실행 파일 등 다양한 파일 형식이 바이너리 데이터로 표현됩니다.
🤔 Base64란 무엇인가요?
바이너리 데이터를 텍스트 형식으로 인코딩하기 위한 방식입니다.
컴퓨터에서 바이너리 데이터(예: 이미지, 오디오 등)를 ASCII 문자만드로 표현하여 전송하거나 저장할 수 있도록 만들어졌습니다.
Base64는 주로 이메일, 웹 API, URL에서 바이너리 데이터를 안전하게 전달하기 위해 사용됩니다.
3️⃣ JWT의 특징.
1️⃣ 자기 포함 토큰.
JWT(JSON Web Token)는 모든 필요한 정보를 자체적으로 포함하므로, 서버가 토큰만 확인해도 인증 및 권한 정보를 알 수 있습니다.
이를 통해 세션 저장소 없이도 사용자 인증을 관리할 수 있습니다.
🤔 세션 저장소(Session Store)란 무엇일까요?
사용자 세션 정보를 저장하고 관리하는 장소로, 주로 웹 애플리케이션에서 사용자가 로그린한 상태를 유지하거나 사용자와 관련된 데이터를 임시로 보관하기 위해 사용됩니다.
세션 저장소(Session Store)는 서버가 사용자별 상태를 관리할 수 있게 하며, 페이지 간 이동이나 서버 간 요청간에도 사용자 상태를 지속적으로 유지할 수 있도록 돕습니다.
🤔 세션 정보(Session Information)이란 무엇일까요?
웹 애플리케이션에서 특정 사용자와 관련된 상태나 데이터를 말합니다.
세션 정보(Session Information)는 사용자가 웹사이트에 로그인 했을 때부터 로그아웃할 때까지 사용자의 활동이나 상태를 유지하고 추적하기 위해 서버에 저장되며, 페이지 이동이나 새로운 요청이 발생하더라도 사용자 상태가 지속되도록 돕습니다.
🤔 세션(Session)이란 무엇일까요?
웹 애플리케이션에서 특정 사용자와 서버 간의 상태를 유지하는 기간을 의미합니다.
서버가 사용자의 요청들을 하나의 연속적인 흐름으로 식별하고 상태를 지속적으로 추적할 수 있도록 도와줍니다.
사용자가 웹사이트에 접속하고 로그아웃하거나 일정 시간 동안 활동이 없으면 세션(Session)이 종료되는 방식으로, 세션(Session)은 사용자가 웹 애플리케이션에 접속해 있는 동안의 상태를 관리하는 역할을 합니다.
2️⃣ 변조 방지.
JWT(JSON Web Token)는 서명(Signature)을 포함하고 있어, 토큰이 변조되지 않았음을 검증할 수 있습니다.
토큰의 정보가 변경될 경우, 서명 검증이 실패하므로 유효하지 않은 토큰으로 간주됩니다.
3️⃣ 짧은 수명.
JWT(JSON Web Token)는 보안상의 이유로 일반적으로 짧은 유효 기간을 갖습니다.
만료된 토큰은 다시 인증을 요청해야 하므로, 주기적으로 재발급을 통해 보안을 유지할 수 있습니다.
4️⃣ 비상태성(Stateless)
JWT(JSON Web Token)는 서버에 상태를 저장하지 않는 비상태성(Stateless) 토큰이므로, 확장성과 성능이 요구되는 환경에서 효과적입니다.
🤔 비상태성(Stateless)란 무엇일까요?
각 요청이 이전 요청이나 이후 요청과 독립적으로 처리되는 특성을 의미합니다.
비상태적인 시스템에서는 각 요청에 필요한 모든 정보가 요청 자체에 포함 되어야 하며, 서버는 요청을 처리할 때 이전 상태나 세션(Session)을 기억하지 않고 매번 새로운 요청으로 처리합니다.
서버는 토큰을 확인하기만 하면 되므로, 세션(Session) 관리가 필요 없는 애플리케이션에 적합합니다.
4️⃣ JWT의 단점.
1️⃣ 서버에서 토큰 무효화가 어렵다.
JWT(JSON Web Token)는 서버에서 상태를 관리하지 않으므로, 토큰을 발급한 후 특정 토큰을 서버에서 무효화하기 어렵습니다.
🤔 토큰 무효화(Token Invalidation)란 무엇일까요?
기존에 발급된 토큰을 더 이상 유효하지 않도록 만드는 과정을 의미합니다.
무효화된 토큰은 사용자가 해당 토큰을 통해 인증이나 권한을 요청할 때 거부되며, 주로 로그아웃 처리, 보안상의 이유, 토큰 재발급 시에 사용됩니다
이를 해결하기 위해 블랙리스트나 짧은 만료 기간을 사용하여 보안을 강화할 수 있습니다.
🤔 블랙리스트(Blacklist)
특정 조건을 충족하지 못해 접근이 제한된 목록으로, 허용되지 않는 항목이나 사용자를 차단하고 관리하기 위해 사용됩니다.
IT 보안, 네트워크, 인증 시스템 등에서 자주 활용되며, 보안과 권한 관리를 위해 불법적이거나 신뢰할 수 없는 대상을 식별하고 차단하는데 도움이 됩니다.
JWT에서의 블랙리스트(Blacklist)는 특정 JWT 토큰을 무효화하기 위해 차단 목록에 추가하는 방식을 의미합니다.
블랙리스트에 추가된 토큰은 더 이상 유효하지 않은 것으로 간주되어, 해당 토큰으로 인증 요청을 보내면 인증이 거부되거나 접근이 제한됩니다.
이는 로그아웃 처리, 토큰 유효기간 이내에 강제 무효화, 보안상 이유로 탈취된 토큰을 차단해야 할 때 유용합니다.
2️⃣ 토큰의 크기가 크다.
JWT(JSON Web Token)는 서명(Signature)과 페이로드(Payload)를 포함하므로 크기가 큰 편입니다.
이는 네트워크 트래픽에 영향을 줄 수 있습니다.
🤔 네트워크 트래픽(Network Traffic)이란 무엇일까요?
네트워크를 통해 전송되는 모든 데이터의 흐름을 의미합니다.
이는 사용자가 인터넷을 통해 주고받는 데이터, 애플리케이션 간의 통신, 서버 간 데이터 교환 등 네트워크 상의 모든 데이터를 포함하며,
보통 초당 전송되는 데이터의 양으로 측정합니다.
🤔 데이터의 흐름이란 무엇일까요?
네트워크 트래픽(Network Traffic)에서의 데이터 흐름은 네트워크 상에서 데이터가 전송되는 과정과 방향을 의미합니다.
네트워크의 각 장치가 서로 데이터를 주고받으며 이동하는 경로를 통해 사용자 간의 통신, 데이터 요청, 파일 전송 등이 이루어집니다.
이 데이터 흐름은 네트워크를 통해 어떻게 데이터가 오가는지 보여주며, 일반적으로 패킷(Packet)이라는 단위로 분할되어 전송됩니다.
🤔 데이터 흐름 방향이란 무엇일까요?
데이터 흐름은 상향 흐름(upload)과 하향 흐름(download)으로 구분되며, 일반적으로 클라이언트와 서버 간에 주고받습니다.
상향 흐름(Upload)는 클라이언트가 서버로 데이터를 보내는 경우를 말하며, 파일의 업로드를 예로 들 수 있습니다.
하향 흐름(Download)는 서버가 클라이언트로 데이터를 보내는 경우를 말하며, 웹페이지 로딩, 파일 다운로드 등을 예로 들 수 있습니다.
🤔 네트워크 상에서 데이터가 전송되는 과정이란 무엇일까요?
네트워크 상에서 데이터가 전송되는 과정은 발신자(출발지)에서 수신자(목적지)까지 데이터를 패킷(Packet)으로 나누고, 이를 여러 네트워크 장치를 거쳐 전달하는 일련의 절차를 의미합니다.
이 과정은 여러 단계로 나뉘며, 프로토콜 스택을 통해 데이터가 송신 장치에서 수신 장치로 안전하고 정확하게 전송되도록 관리됩니다.
🤔 프로토콜 스택(Protocol Stack)이란 무엇일까요?
네트워크 통신에서 데이터를 송수신하기 위해 계층별로 역할을 나눠 프로토콜을 구성한 집합입니다.
이 스택은 네트워크 통신 과정에서 발생하는 다양한 작업을 논리적으로 나누고, 각 계층에 특정 역할을 부여하여 데이터를 전송, 처리, 수신할 수 있도록 설계되었습니다.
각 계층은 독립적이며, 상호 작용을 통해 최종적으로 데이터가 목적지에 도달합니다.
3️⃣ 탈취된 토큰의 악용 가능성.
만료되지 않은 JWT(JSON Web Token)가 탈취된 경우, 공격자가 이를 사용하여 사용자로 위장할 수 있습니다.
이를 방지하기 위해 HTTPS를 통한 전송 및 짧은 유효 기간 설정이 중요합니다.
🤔 HTTPS를 통한 전송이란 무엇일까요?
웹 브라우저와 서버 간의 통신이 암호화된 상태로 이루어지는 방식입니다.
HTTPS는 HTTP에 SSL(Secure Sockets Layer) 또는 TLS(Transport Layer Security) 암호화 계층을 추가한 프로토콜로,
사용자가 웹사이트와 주고받은 데이터를 제3자가 볼 수 없도록 보호합니다.
이를 통해, 웹사이트 로그인 정보, 신용카드 번호, 개인 정보 등 민감한 데이터가 안전하게 전송될 수 있습니다.
🤔 HTTP(HyperText Transfer Protocol)이란 무엇일까요?
웹 브라우저와 웹 서버 간에 하이퍼텍스트(HyperText)를 주고받기 위한 프로토콜로, 인터넷 상에서 데이터를 전송하는 표준 규약입니다.
HTTP(HyperText Transfer Protocol)는 웹 페이지, 이미지, 비디오와 같은 리소스를 전송하는데 사용되며, 클라이언트-서버 구조를 기반으로 작동합니다.
웹 브라우저가 클라이언트 역할을 하고, 웹 서버는 클라이언트 요청을 처리하여 필요한 정보를 제공하는 역할을 합니다.
🤔 하이퍼텍스트(HyperText)란 무엇일까요?
특정 단어, 문장, 이미지 등을 클릭하면 관련된 다른 문서나 페이지로 연결되는 방식의 텍스트를 의미합니다.
웹의 핵심적인 개념으로, 하이퍼텍스트(HyperText)는 사용자가 링크를 클릭하여 서로 관련된 정보나 페이지로 자유롭게 이동할 수 있게 도와줍니다.
일반 텍스트와 달리, 하이퍼텍스트(HyperText)는 문서 간의 관계를 쉽게 연결하고 참조할 수 있는 기능을 제공하여 비선형적인 정보 탐색을 가능하게 합니다.
🤔 SSL(Secure Sockets Layer)란 무엇일까요?
네트워크 상에서 데이터를 안전하게 전송하기 위한 보안 프로토콜로, 클라이언트와 서버 간의 데이터 통신을 암호화하여 데이터의 기밀성, 무결성, 인증을 보장합니다.
SSL(Secure Sockets Layer)은 웹사이트가 HTTPS 프로토콜을 사용할 수 있게 하며, 주로 인터넷 상의 민감한 정보(예: 로그인 정보, 결제 정보) 보호에 사용됩니다.
🤔 TLS(Transport Layer Security)란 무엇일까요?
인터넷 상에서 데이터를 안전하게 전송하기 위한 보안 프로토콜로, SSL(Secure Sockets Layer)의 후속 버전입니다.
TLS(Transport Layer Security)는 클라이언트와 서버 간의 데이터 전송을 암호화하여 기밀성과 무결성을 보장하며, 현재 대부분의 HTTPS 연결에 사용됩니다.
TLS(Transport Layer Security)는 데이터가 중간에 탈취되거나 조작되는 것을 방지하여, 민감한 정보를 안전하게 전송할 수 있도록 돕습니다.
5️⃣ JWT의 사용 예시(Java)
Java에서 JWT를 사용하려면, JJWT(Java JWT) 라이브러리를 사용하여 토큰을 생성하고 검증할 수 있습니다.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
public class JwtExample {
private static final String SECRET_KEY = "mySecreKey";
// JWT 토큰 생성
public static String createToken(String subject) {
return Jwts.builder()
.setSubject(subject) // 사용자 식별자(id)
.setIssuedAt(new Date()) // 발행 시간
.setExpiration(new Date(System.currentTimeMillis() * 3600000)) // 만료 시간(expiration)-1시간 후
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 서명 알고리즘 및 키
.compact();
}
// JWT 토큰 검증 및 정보 추출
public static Claims parseToken(String token) {
return Jwts.parse()
.setSigningKey(SECRET_KEY)
.parseClaimsJwt(token)
.getBody();
}
public static void main(String[] args) {
// 토큰 생성
String token = createToken("user123");
System.out.println("JWT Token: " + token);
// 토큰 검증 및 정보 추출
Claims claims = parseToken(token);
System.out.println("Subject: " + clamis.getSubject());
}
}
👉 설명.
createToken 메서드는 사용자 정보를 기반으로 JWT 토큰을 생성합니다.
parseToken 메서드는 생성된 토큰을 검증하고 페이로드의 정보를 추출합니다.
이 예제에서는 user123이라는 사용자 식별자로 토큰을 생성하고, 생성된 토큰에서 사용자 식별자(subject)를 추출합니다.
6️⃣ JWT의 사용 사례.
1️⃣ 사용자 인증.
웹 애플리케이션에서 로그인 후 사용자 정보를 확인하는데 JWT를 사용하여, 추가적인 세션 관리 없이 인증을 처리할 수 있습니다.
2️⃣ API 인증.
RESTful API에서는 클라이언트가 JWT를 포함하여 요청을 보내면, 서버는 토큰을 검증하여 클라이언트의 권한을 확인할 수 있습니다.
🤔 RESTful API란 무엇일까요?
REST(Representational State Transfer) 아키텍처 스타일을 기반으로 설계된 API로, 웹에서 클라이언트와 서버 간에 리소스를 효율적으로 주고받기 위한 규칙과 원칙을 따르는 API입니다.
RESTful API는 HTTP 프로토콜을 사용하여 웹의 리소스(Resource)를 URL을 통해 접근하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용해 해당 리소스를 조작합니다.
🤔 REST(Representational State Transfer) 아키텍처란 무엇일까요?
웹 기반의 분산 시스템을 설계하기 위한 아키텍처 스타일로, 리소스(Resource)를 기반으로 클라이언트와 서버 간의 상태 정보를 주고받는 방식입니다.
REST는 HTTP 프로토콜을 기반으로 설계되었으며, 웹에서 데이터를 효율적이고 확장 가능하게 주고받기 위해 권장되는 원칙과 제약 조건을 정의합니다.
REST 아키텍처 스타일은 주로 웹 API 설계에 적용되며, 웹 애플리케이션에서 클라이언트가 서버의 리소스에 접근할 때 일관성 있는 방식을 제공합니다.
REST는 특정한 표준이나 프로토콜이 아니며, 설계 원칙에 따른 웹 서비스 구조를 의미합니다.
🤔 REST에서의 리소스(Resource)란 무엇일까요?
웹에서 고유하게 식별할 수 있는 모든 데이터나 객체를 의미합니다.
REST 아키텍처에서는 리소스(Resource)를 통해 웹 서비스가 클라이언트에게 제공하는 데이터나 기능을 추상화한 개체로, 리소스는 URI(Uniform Resource Identifier)를 통해 고유하게 식별됩니다.
RESTful API에서는 이러한 리소스(Resource)에 대한 데이터를 HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 접근하고 조작합니다.
🤔 URI(Uniform Resource Identifier)란 무엇일까요?
웹에서 특정 리소스를 식별하고 위치를 지정하는 고유한 문자열입니다.
웹 리소스를 유일하게 식별하여 접근할 수 있도록 설계된 URI(Uniform Resource Identifier)는 웹 주소를 지정할 때 사용되며,
우리가 흔히 접하는 URL도 URI의 한 종류입니다.
URI는 RESTful API에서 리소스를 식별하는 중요한 요소로, 사용자가 원하는 리소스를 정확히 찾을 수 있게 합니다.
🤔 분산 시스템(Distributed System)이란 무엇일까요?
여러 대의 컴퓨터나 서버가 네트워크를 통해 연결되어 하나의 시스템처럼 동작하는 구조를 말합니다.
분산 시스템(Distributed System)에서는 하나의 작업을 여러 장치에서 나누어 처리하여, 데이터 처리 속도와 효율성을 높이고, 단일 장애 지점을 줄여 시스템의 신뢰성을 강화할 수 있습니다.
3️⃣ 마이크로서비스 간의 통신.
JWT는 서버 간 인증에도 사용할 수 있습니다.
마이크로서비스 아키텍처에서 각 서비스가 독립적으로 JWT를 검증할 수 있으므로, 안전하고 효율적인 인증 관리가 가능합니다.
🤔 마이크로서비스 아키텍처(Microservices Architecture)란 무엇일까요?
애플리케이션을 여러 개의 작은 독립적인 서비스로 나눠서 개발하고 배포하는 방식을 의미합니다.
각 마이크로서비스(Microservices)는 특정 기능이나 비즈니스 로직을 독립적으로 수행하며, 서로 독립적으로 개발, 배포, 확장, 유지보수할 수 있는 특성이 있습니다.
이 아키텍처는 대규모 애플리케이션에서 유연선과 확장성을 극대화할 수 있도록 설계되었습니다.
7️⃣ 요약.
JWT(JSON Web Token)은 안전하고 간편한 인증과 정보 전송을 위한 토큰으로, 디지털 서명을 통해 토큰이 변조되지 않았음을 검증할 수 있습니다.
JWT는 주로 사용자 인증과 API 접근 제어 등에 사용되며, 자기 포함 토큰이므로 별도의 세션 저장소가 필요하지 않습니다.
Java에서 JWT를 생성하고 검증하려면 JJWT 라이브러리와 같은 도구를 사용할 수 있으며, 이를 통해 간편하게 토큰을 관리할 수 있습니다.
-
🍃[Spring] 회원 가입 - 유저 생성 API 개발하기.
🍃[Spring] 회원 가입 - 유저 생성 API 개발하기.
1️⃣ API 설계하기.
1️⃣ 사용자를 등록하기 위한 API 명세.
회원 가입 웹 UI가 먼저 만들어져 있습니다.
그러므로 프론트엔드에서 사용한 API, 즉 기능들을 채워 넣어야 합니다.
회원 가입을 위한 API 명세는 다음과 같습니다.
HTTP Method : POST
HTTP Path : /register
Http Body (JSON)
{
"username": String (null 불가능),
"email": String (null 불가능),
"password": String (null 불가능),
"confirmPassword": String (null 불가능)
}
결과 반환 X (HTTP 상태 200OK 이면 충분함)
🙋♂️ API(Applicatioon Programming Interface)란 무엇일까?
2️⃣ UserController 생성하기.
우선 UserController를 생성한 후 내부에 다음과 같이 코드를 생성해줍니다.
package com.kobe.signUpApp.controller.user;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@PostMapping("/register")
public void registerUser() {
}
}
3️⃣ dto 패키지 -> user 패키지 -> request 패키지 -> UserCreateRequest 클래스 생성하기
아래와 같이 dto 객체인 userCreateRequest 클래스를 생성해줍니다.
package com.kobe.signUpApp.dto.user.request;
public class UserCreateRequest {
private String username;
private String email;
private String password;
private String confirmPassword;
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
public String getConfirmPassword() {
return confirmPassword;
}
}
4️⃣ UserController 내부 registerUser 메서드 리팩토링.
POST API에서 들어온 HTTP Body를 위에서 만든 UserCreateRequest 클래스로 변환하기 위해서 @RequestBody 어노테이션을 사용해줍니다.
package com.kobe.signUpApp.controller.user;
import com.kobe.signUpApp.dto.user.request.UserCreateRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@PostMapping("/register")
public void registerUser(@RequestBody UserCreateRequest request) {
}
}
여기까지 API의 구조를 잡았습니다.
5️⃣ 실제 유저가 저장되도록 구현하기.
유저를 저장하기 위하여 User라는 객체를 만들어서 저장할 것 입니다.
다음과 같은 과정을 거쳐서 만들어 볼 것 입니다.
먼저, domain 패키지 내부에 user라는 패키지를 만들어 줍니다.
user 패키기 내부에 User 클래스를 만들어 줍니다.
@PostMapping("/register")
public void registerUser(@RequestBody UserCreateRequest request) {
}
위 API가 사용되서 유저가 저장되어야 한다면 User 객체를 만들어서 실제 리스트에 저장을 할 것 입니다.
User의 경우에는 다음과 같은 필드가 포함됩니다.
username
email
password
confirmPassword
package com.kobe.signUpApp.domain.user;
public class User {
private String username;
private String email;
private String password;
private String confirmPassword;
public User(
String username,
String email,
String password,
String confirmPassword
) {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException(String.format("잘못된 username(%s)이 들어왔습니다.", username));
} else if (email == null || email.isBlank()) {
throw new IllegalArgumentException(String.format("잘못된 email(%s)이 들어왔습니다.", email));
} else if (password == null || password.isBlank()) {
throw new IllegalArgumentException(String.format("잘못된 password(%s)이 들어왔습니다.", password));
} else if (confirmPassword == null || confirmPassword.isBlank()) {
throw new IllegalArgumentException(String.format("잘못된 confirmPassword(%s)이 들어왔습니다.", confirmPassword));
}
this.username = username;
this.email = email;
this.password = password;
this.confirmPassword = confirmPassword;
}
}
User 객체를 만들 때, 생성자(Constructor)를 통해 필드에 값을 넣어줍니다.
이 때, null이 들어 와서는 안되는 필드들을 위해 조건문을 사용하여 null 값이 들어왔을 때 IllegalAragumentException 예외를 던집니다(throw).
즉, null 값이 들어올 경우 예외가 던져지므로 User 객체 자체가 생성되지 않기 때문에 저장도 되지 않습니다.
그리고 예외를 던지는 과정에서 어떤 값이 들어왔는지 알려주면 좋기 때문에 String.format을 사용하여 예외의 메세지로 담을 수 있게끔 처리했습니다.
6️⃣ User 클래스를 인스턴스화 시켜 저장되게 만들기.
UserController 클래스 안에 다음과 같이 코드를 작성해줍니다.
package com.kobe.signUpApp.controller.user;
import com.kobe.signUpApp.domain.user.User;
import com.kobe.signUpApp.dto.user.request.UserCreateRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class UserController {
private final List<User> users = new ArrayList<>();
@PostMapping("/register")
public void registerUser(@RequestBody UserCreateRequest request) {
users.add(new User(request.getUsername(), request.getEmail(), request.getPassword(), request.getConfirmPassword()));
}
}
먼저 private final List<User> users = new ArrayList<>(); 로 리스트를 만들어 줍니다.
registerUser라는 함수가 호출되면, 즉 POST/registerUser가 호출될 경우 users.add(new User(request.getUsername(), request.getEmail(), request.getPassword(), request.getConfirmPassword())); 가 실행 됩니다.
User를 만들때는 API를 통해 들어온 유저이름, 이메일, 비밀번호, 확인용 비밀번호를 집어 넣어줍니다
즉, 위 코드는 “API를 통해 들어온 유저이름, 이메일, 비밀번호, 확인용 비밀번호를 집어 넣는 코드입니다.”
7️⃣ 요약.
POST/registerUser가 호출될 경우, UserController 내부에 있는 registerUser 함수가 실행됩니다.
이 때, JSON 형식으로 HTTP Body에 username, email, password, confirmPassword가 들어오면 UserCreateRequest의 객체의 값으로 맵핑되고 public void registerUser(@RequestBody UserCreateRequest request)의 request는 새로운 User를 만드는데 사용됩니다.
이렇게 새롭게 만드는데 사용된 User 객체는 리스트에 저장됩니다.
registerUser 함수가 예외 없이 끝나게 된다면 200 OK를 반환하게 됩니다.
-
💾 [CS] 동시성 프로그래밍(Concurrency Programming)이란 무엇일까요?
💾 [CS] 동시성 프로그래밍(Concurrency Programming)이란 무엇일까요?
동시성 프로그래밍(Concurrency Programming)은 여러 작업을 동시에 수행할 수 있도록 프로그램을 설계하는 기법입니다.
동시성(Concurrency)은 작업의 실행 흐름을 겹치게 하거나 병렬로 처리하여, 시스템의 효율성을 높이고 처리 시간을 줄이는 데 중점을 둡니다.
동시성 프로그래밍(Concurrency Programming)은 멀티스레딩, 멀티프로세싱을 포함하여, 비동기 프로그래밍과 같은 다양한 기법을 사용하여 시스템 자원을 최대한 활용할 수 있도록 합니다.
1️⃣ 동시성과 병렬성의 차이.
동시성과 병렬성은 종종 혼동되지만, 서로 다른 개념입니다.
1️⃣ 동시성(Concurrency)
동시성(Concurrency)은 여러 작업이 실행되는 것처럼 보이게 하는 것을 의미합니다.
실제로는 작업 간의 실행 시간을 쪼개어 번갈아 가면서 실행하여, 여러 작업이 동시에 진행되는 것처럼 보이게 만듭니다.
예를 들어, 하나의 CPU 코어에서 두 개의 작업을 번갈아가며 빠르게 실행하면, 두 작업이 동시에 실행되는 것처럼 느껴집니다.
이는 멀티태스킹(Multitasking)의 개념과 유사합니다.
2️⃣ 병렬성(Parallelism)
병렬성(Parallelism)은 여러 작업을 동시에 실행하는 것을 의미합니다.
동시성과 달리, 실제로 여러 CPU 코어에서 여러 작업을 동시에 처리합니다.
병렬성(Parallelism)은 멀티코어 프로세서 환경에서 주로 이루어지며, 하나의 작업을 여러 조각으로 나눠 여러 코어에서 동시에 처리할 수 있게 해줍니다
2️⃣ 동시성 프로그래밍(Concurrency Programming)의 필요성
1️⃣ 효율적인 자원 사용.
컴퓨터 시스템은 CPU, 메모리, I/O 장치와 같은 다양한 자원이 있습니다.
동시성 프로그래밍(Concurrency Programming)을 통해 이러한 자원을 효율적으로 사용할 수 있습니다.
예를 들어, 파일을 읽거나 네트워크 요청을 기다리는 동안 CPU가 유후 상태에 머무르지 않도록, 다른 작업을 실행할 수 있습니다.
📝 유후 상태(Idle State)
CPU가 현재 실행할 작업이 없어서 대기하고 있는 상태를 의미합니다.
즉, CPU가 아무런 작업도 수행하지 않고 쉬고 있는 상태를 뜻합니다.
컴퓨터 시스템에서 CPU는 항상 작업을 할당받아야 제대로 작동할 수 있습니다.
그러나 입출력 작업이 완료되기를 기다리거나, 다음 명령어가 준비되지 않은 경우 등, 특정 상황에서는 CPU가 유후 상태로 머물게 됩니다.
2️⃣ 빠른 응답성.
동시성 프로그래밍(Concurrency Programming)은 프로그램의 응답성을 향상시킵니다.
예를 들어, 사용자가 버튼을 클릭할 때마다 새로운 요청이 처리되도록 하고, 메인 스레드가 다른 작업을 수행하는 동안에도 UI가 멈추지 않게 할 수 있습니다.
이는 특히 웹 서버와 같은 환경에서 중요합니다.
여러 사용자의 요청을 동시에 처리하여 빠르게 응답할 수 있습니다.
3️⃣ 배치 처리 및 병렬 계산.
동시성 프로그래밍(Concurrency Programming)은 대량의 데이터를 병렬로 처리할 때 유리합니다.
예를 들어, 빅데이터 처리를 위해 여러 노드에서 데이터를 동시에 처리할 수 있습니다.
📝 노드(Node)
컴퓨터 네트워크에서 데이터를 처리하거나 전송하는 장치나 시스템을 의미합니다.
노드는 네트워크를 구성하는 개별적인 장치로, 서버, 컴퓨터, 라우터, 스위치, 또는 클라이언트 등이 모두 노드가 될 수 있습니다.
분산 시스템에서 노드는 각각 독립적으로 작업을 수행하거나, 서로 협력하여 큰 작업을 분담할 수 있습니다.
3️⃣ 동시성 프로그래밍(Concurrency Programming)의 주요 개념.
1️⃣ 스레드(Thread)
스레드는 프로세스 내에서 실행되는 작은 실행 단위입니다.
한 프로세스는 여러 스레드를 가질 수 있으며, 이들 스레드는 메모리와 자원을 공유합니다.
동시성 프로그래밍(Concurrency Programming)에서 멀티스레딩(Multithreading)을 통해 여러 스레드를 동시에 실행하여 작업을 동시에 처리할 수 있습니다.
2️⃣ 락(Lock)
여러 스레드가 공유 자원에 동시에 접근하면 데이터 충돌(Race Condition)이 발생할 수 있습니다.
이를 방지하기 위해, 공유 자원에 접근할 때 락을 사용하여 한 번에 하나의 스레드만 자원에 접근하도록 합니다.
하지만 락을 잘못 사용할 경우 데드락(DeadLock)이나 라이블록(Livelock) 같은 문제가 발생할 수 있스므로, 주의가 필요합니다.
3️⃣ 뮤텍스(Mutex)와 세마포어(Semaphore)
뮤텍스(Mutex)는 두 개 이상의 스레드가 동시에 자원에 접근하는 것을 방지하기 위해 사용되는 락의 일종입니다.
특정 스레드가 자원을 사용하면, 다른 스레드는 그 자원이 해제될 때까지 대기해야 합니다.
세마포어(Semaphore)는 리소스의 접근 가능한 수량을 관리하여, 여러 스레드가 리소스레 접근하도록 제어할 수 있습니다.
제한된 수의 스레드만 리소스에 접근할 수 있게끔 허용합니다.
4️⃣ 비동기 프로그래밍(Asynchronous Programming)
비동기 프로그래밍(Asynchronous Programming)은 동시성 프로그래밍(Concurrency Programming)의 한 형태로, 특정 작업이 완료될 때 까지 대기하지 않고 다른 작업을 먼저 수행하도록 합니다.
예를 들어, 네트워크 요청을 보내고 응답을 기다리는 동안, 프로그램은 다른 작업을 수행할 수 있습니다.
Java에서는 CompletableFuture, JavaScript에서는 Promise를 사용하여 비동기 프로그래밍(Asynchronous Programming)을 쉽게 구현할 수 있습니다.
4️⃣ 동시성 프로그래밍(Concurrency Programming)의 예시.
1️⃣ 멀티스레딩 예시.
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + ": " + i);
try {
Thread.sleep(500); // 0.5초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MYThread();
t1.start(); // t1 스레드 시작
t2.start(); // t2 스레드 시작
}
}
위 코드는 두 개의 스레드를 생성하고, 동시에 실행시켜 동시성을 구현하는 예시입니다.
2️⃣ 비동기 프로그래밍 예시.
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
System.out.println("비동기 작업 완료!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("메인 스레드 작업 실행 중...");
future.join(); // 비동기 작업이 끝날 때까지 대기
}
}
이 코드는 비동기적으로 실행되는 작업을 만들고, 메인 스레드가 작업을 기다리지 않고 다른 작업을 수행할 수 있게 합니다.
5️⃣ 동시성 프로그래밍(Concurrency Programming)의 장점.
1️⃣ 자원 효율성 극대화.
CPU가 작업을 대기하는 동안 유휴 상태로 있지 않고, 다른 작업을 수행할 수 있어 시스템 자원의 효율성을 높일 수 있습니다.
2️⃣ 빠른 응답성
여러 작업을 동시에 수행하거나, 특정 작업을 기다리는 동안에도 프로그램이 응답할 수 있도록 하여, 사용자의 경험을 개선 합니다.
3️⃣ 병렬 처리
여러 코어를 사용하는 멀티코어 프로세서 환경에서 병렬 처리를 통해 작업 속도를 크게 향상시킬 수 있습니다.
6️⃣ 동시성 프로그래밍(Concurrency Programming)의 단점.
1️⃣ 복잡성 증가.
여러 스레드가 동시에 작업을 수행할 때 동기화 문제가 발생할 수 있어, 프로그램의 복잡성이 증가합니다.
개발자는 데드락(DeadLock), 레이스 컨디션(Race Condition) 등 여러 문제를 해결해야 합니다.
2️⃣ 디버깅의 어려움.
동시성 코드의 디버깅은 다른 시검에 따라 다른 결과가 발생할 수 있기 때문에 예측하기 어려운 버그가 나타날 수 있습니다.
3️⃣ 성능 오버헤드.
스레드를 생성하고 관리하는 데에도 비용이 발생하며, 동기화를 위해 락을 사용하면 성능이 저하될 수 있습니다.
6️⃣ 요약.
동시성 프로그래밍(Concurrency Programming)은 여러 작업을 동시에 수행할 수 있도록 설계된 프로그래밍 방식으로, 멀티스레딩(Multilthreading), 비동기 프로그래밍(Asyncronous Programming), 멀티프로세싱(Multiprocessing)을 포함하여 시스템 자원을 효율적으로 사용하고 프로그램의 응답성을 높이는 것을 목표로 합니다.
동시성 프로그래밍(Concurrency Programming)은 CPU가 작업을 대기하는 동안 유휴 상태로 있지 않고, 다른 작업을 수행하여 성능을 최적화합니다.
그러나 코드의 복잡성, 디버깅의 어려움, 성능 오버헤드 등고 함께 고려해야 합니다.
-
💾 [CS] CPU 바운드 작업(CPU-Bound Task)이란 무엇일까요?
💾 [CS] CPU 바운드 작업(CPU-Bound Task)이란 무엇일까요?
CPU 바운드 작업(CPU-Bound Task)은 프로그램의 실행 속도가 CPU의 처리 속도에 의해 제한되는 작업을 의미합니다.
즉, 연산이나 계산 작업이 많아서 CPU가 대부분의 시간을 계산 처리에 사용하며, CPU의 성능이 전체 작업의 성능을 결정짓는 상황입니다.
1️⃣ CPU 바운드 작업(CPU-Bound Task)의 특징.
1️⃣ 복잡한 계산 작업.
CPU 바운드 작업(CPU-Bound Task)은 일반적으로 복잡한 수학 계산, 데이터 처리, 암호화, 머신 러닝 등 많은 연산을 필요로 하는 작업이 포함됩니다.
CPU가 계속해서 계산 작업을 수행하며, 다른 하드웨어(예: 디스크, 네트워크)의 속도에 의존하지 않고 CPU의 속도에 의해 성능이 결정됩니다.
2️⃣ CPU 사용률이 높음.
CPU 바운드 작업(CPU-Bound Task)은 CPU의 연산 자원(CPU 코어(CPU Core)와 클럭 속도(Clock Speed))을 최대한 활용합니다.
📝 CPU Core
중앙처리장치(Central Processing Unit, CPU) 내부에서 독립적으로 작업을 수행할 수 있는 처리 단위를 의미합니다.
각 코어(Core)는 자신만의 연산 장치(ALU), 레지스터, 제어 장치 등을 갖추고 있어, 프로그램의 명령을 독립적으로 처리할 수 있습니다.
현대의 CPU는 여러 개의 Core(Multi Core)를 포함하고 있어, 동시에 여러 작업을 병렬로 처리할 수 있습니다.
📝 클럭 속도(Clock Speed)
클럭 속도(Clock Speed)는 CPU가 작업을 수행하는 속도를 나타내는 지표로, 1초 동안 CPU가 실행할 수 있는 명령어 사이클의 수를 의미합니다.
클럭 속도(Clock Speed)는 헤르츠(Hz) 단위로 측정되며, 메가헤르츠(MHz) 또는 기가헤르츠(GHz)로 표현됩니다.
예를 들어, 1GHz는 CPU가 1초에 10억 번의 명령어 사이클을 처리할 수 있다는 의미입니다.
클럭 속도(Clock Speed)가 높을수록 CPU가 더 빠르게 작동하여, 더 많은 작업을 짧은 시간 안에 처리할 수 있습니다.
프로그램을 실행할 때 CPU 사용률(CPU Utilization)이 높게 나타나며, CPU 성능이 전체 작업 속도에 큰 영향을 미칩니다.
2️⃣ CPU 바운드 작업(CPU-Bound Task)의 예.
1️⃣ 이미지 처리.
대량의 이미지를 변환하거나, 필터를 적용하는 작업은 픽셀 단위로 많은 계산을 필요로 합니다.
이러한 작업은 CPU의 계산 속도에 의해 처리 속도가 결정됩니다.
2️⃣ 비디오 인코딩/디코딩.
비디오 파일을 인코딩하거나 디코딩하는 작업은 대량의 데이터 처리가 필요하고, 복잡한 알고리즘을 통해 프레임을 압축하거나 복원해야 하기 때문에 CPU 바운드 작업(CPU-Bound Task)에 해당합니다.
3️⃣ 암호화/복호화.
데이터를 암호화하거나 복호화하는 과정은 CPU가 복잡한 계산을 수행해야 합니다.
이는 주로 보안과 관련된 작업에서 자주 사용됩니다.
4️⃣ 과학 계산 및 데이터 분석.
데이터 분석, 수학적 모델링, 시뮬레이션 등 수많은 계산을 필요로 하는 작업도 CPU 바운드 작업입니다.
예를 들어, 대규모 데이터셋에서 통계적인 계산을 하는 작업은 CPU의 처리 성능에 크게 의존합니다.
5️⃣ 게임 로직 및 물리 엔진.
게임 내의 물리 연산, AI 처리, 게임 로직 등은 많은 계산을 필요로 하며, 실시간으로 계산을 수행해야 하기 때문에 CPU 바운드 작업에 속합니다.
3️⃣ CPU 바운드 vs I/O 바운드
1️⃣ CPU 바운드
프로그램의 성능이 CPU 처리 속도에 의해 제한됩니다.
복잡한 계산이나 연산 작업을 수행하는 경우 CPU 바운드가 됩니다.
예: 이미지 처리, 비디오 인코딩, 암호화 등.
2️⃣ I/O 바운드
프로그램의 성능이 입출력(I/O) 작업의 속도에 의해 제한됩니다.
네트워크 통신, 파일 읽기/쓰기, 데이터베이스 쿼리 등 외부 장치의 속도에 따라 성능이 결정됩니다.
예: 파일 다운로드, 데이터베이스 조회, 웹 페이지 요청 등
4️⃣ CPU 바운드 작업의 최적화.
1️⃣ 병렬 처리(Parallel Processing)
CPU 바운드 작업은 멀티코어 CPU를 사용하여 여러 코어에서 동시에 작업을 수행하도록 하여 성능을 향상시킬 수 있습니다.
Java에서는 멀티스레딩(Multithreading)을 사용하여 여러 스레드에서 동시에 작업을 처리하도록 하거나 Fork/Join Framework를 사용하여 태스크를 분할하고 병렬로 처리할 수 있습니다.
👉 예시(Java에서 Fork/Join Framework)
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class SumTask extends RecursiveTask<Long> {
private final int[] array;
private final int start;
private final int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= 10) { // 작은 작업은 직접 계산
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else { // 큰 작업은 분할
int mid = (start + end) / 2;
SumTask
}
}
public static void main(String[] args) {
int[] array = new int[1000];
ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
long sum = pool.invoke(task);
System.out.println("Sum: " + sum);
}
}
2️⃣ 연산 최적화.
반복적인 계산을 줄이기 위해 알고리즘을 최적화하거나, 복잡한 연산을 간단하게 변경하여 성능을 향상 시킬 수 있습니다.
예를 들어, 데이터가 이미 정렬되어 있는지 확인하는 작업에서는 매번 정렬을 수행하기보다 정렬 여부를 미리 체크하여 불필요한 연산을 줄이는 방식으로 최적화할 수 있습니다.
3️⃣ 캐싱(Caching)
반복적으로 계산되는 값들을 캐시 메모리에 저장해두고, 필요할 때마다 다시 계싼하지 않고 저장된 값을 사용하여 성능을 높일 수 있습니다.
CPU 바운드 작업에서는 중복된 계산이 발생하지 않도록 메모이제이션(Memoization) 기법을 사용하여 성능을 향상시킬 수 있습니다.
5️⃣ 요약.
CPU 바운드 작업은 프로그램 성능이 CPU의 연산 처리 속도에 의해 제한되는 작업을 의미합니다.
이러한 작업은 복잡한 계산, 데이터 처리, 암호화 등 CPU가 많은 연산을 수행해야 하는 경우가 많습니다.
CPU 바운드 작업을 최적화하기 위해서는 병렬 처리, 연산 최적화, 캐싱 등의 기법을 활용하여 CPU 자원을 효율적으로 사용하고, 성능을 최대한 끌어올릴 수 있습니다.
CPU 바운드 작업은 I/O 바운드 작업과 대조적으로, 연산 작업이 중심이 되는 상황에서 발생합니다.
-
💾 [CS] I/O 바운드 작업(I/O-Bound Task)이란 무엇일까요?
💾 [CS] I/O 바운드 작업(I/O-Bound Task)이란 무엇일까요?
I/O 바운드 작업(I/O-Bound Task)은 입출력(Input/Output) 작업의 속도에 의해 전체 작업의 성능에 제한되는 작업을 의미합니다.
즉, 프로그램이 실행되는 동안 CPU가 데이터를 처리하는 것보다, 데이터를 읽고 쓰는 작업(I/O)에서 더 많은 시간이 소비되는 상황을 말합니다.
1️⃣ I/O 바운드 작업(I/O-Bound Task)의 예.
1️⃣ 파일 입출력.
하드 디스크에 파일을 읽거나 쓰는 작업은 I/O 바운드 작업(I/O-Bound Task)의 대표적인 예입니다.
예를 들어, 대용량 파일을 읽어와서 처리하거나, 결과를 파일에 저장할 때, 디스크의 읽기/쓰기 속도에 따라 작업의 전체 시간이 결정됩니다.
2️⃣ 네트워크 통신.
인터넷에서 데이터를 다운로드하거나 업로드하는 작업은 네트워크 속도에 의존합니다.
예를 들어, 웹 페이지에서 데이터를 가져오거나 API 서버로 요청을 보내는 작업은 네트워크의 지연 시간(latency)과 대역폭(bandwidth)에 의해 제한됩니다.
3️⃣ 데이터베이스 쿼리.
데이터베이스에서 데이터를 조회하거나 삽입하는 작업은 I/O 바운드 작업(I/O-Bound Task)입니다.
디스크에 저장된 데이터를 읽어오거나, 데이터를 저장할 때 디스크 속도와 데이터베이스의 처리 성능이 작업의 속도를 좌우합니다.
2️⃣ I/O 바운드 vs CPU 바운드
1️⃣ I/O 바운드(I/O Bound)
작업의 성능이 I/O 속도에 의해 제한되는 상황입니다.
네트워크, 디스크, 데이터베이스 등의 입출력 장치에서 데이터를 읽거나 쓰는 속도가 전체 성능에 영향을 미칩니다.
예를 들어, 파일 다운로드나 데이터베이스에서 대량의 데이터 조회가 I/O 바운드 작업입니다.
이 경우, CPU는 데이터 처리할 준비가 되어 있어도, 디스크나 네트워크에서 데이터를 받아오는 동안 대기하게 됩니다.
2️⃣ CPU 바운드(CPU Bound)
작업의 성능이 CPU의 처리 속도에 의해 제한되는 상황입니다.
복잡한 계산이나 데이터 처리 작업이 많아 CPU가 대부분의 시간을 계산 작업에 소비할 때 발생합니다.
예를 들어, 암호화, 이미지 처리, 머신 러닝 모델 학습 등이 CPU 바운드 작업입니다.
이 경우, 작업 속도는 CPU 처리 성능에 의해 결정됩니다.
3️⃣ I/O Bound 작업 최적화.
1️⃣ 비동기 프로그래밍(Asynchronous Programming)
비동기 I/O(Asynchronous I/O)는 I/O 작업(I/O Task)이 완료될 때까지 CPU가 대기하지 않고, 다른 작업을 계속 수행항 수 있도록 합니다.
이를 통해 I/O 대기 시간을 줄이고, CPU를 더 효율적으로 사용할 수 있습니다.
예를 들어, 파일을 다운로드하는 동안 다른 작업을 처리할 수 있어, 작업의 전체 처리 속도를 개선할 수 있습니다.
👉 예시 (Java 비동기 I/O)
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
public class AsyncFileRead {
public static void main(String[] args) {
CompletableFuture.runAsync(() -> {
try {
String content = Files.readString(Paths.get("example.txt")), StandardCharsets.UTF_8);
System.out.println(content);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("파일 읽기 요청 완료");
}
}
위 코드는 비동기로 파일을 읽기 때문에, 파일을 읽는 동안 다른 작업을 수행할 수 있습니다.
2️⃣ 멀티스레딩(Multithreading)
여러 스레드(Thread)를 사용하여 동시에 I/O 작업을 수행할 수 있습니다.
이를 통해 I/O 작업이 대기하는 동안 다른 스레드가 CPU를 사용하여 효율성을 높일 수 있습니다.
예를 들어, 네트워크에서 여러 데이터를 동시에 가져와야 할 때, 각 데이터를 개별 스레드(Thread)에서 처리하도록 하여 병렬 처리를 구현할 수 있습니다.
👉 예시(Java 멀티스레딩)
public class MultiThreadedDownload {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> downloadFile("http://example.com/file1"));
Thread thread2 = new Thread(() -> downloadFile("http://example.com/file2"));
thread1.start();
thread2.start();
}
public static void downloadFile(String url) {
// 네트워크 파일 다운로드 작업 수행
System.out.println("Downloading from " + url);
}
}
3️⃣ 캐싱(Caching).
자주 사용하는 데이터를 메모리에 저장해두고, 필요할 때마다 빠르게 접근할 수 있도록 합니다.
이를 통해 디스크나 네트워크에 불필요한 I/O 요청을 줄일 수 있습니다.
예를 들어, 웹 페이지를 로드할 때, 정적인 리소스를 캐싱(Caching)하면 네트워크 요청을 줄이고 더 빠르게 로드할 수 있습니다.
4️⃣ 효율적인 데이터 처리.
데이터를 대량으로 한 번에 처리하여 I/O 요청을 줄입니다.
예를 즐어, 데이터베이스에 한 번에 여러 행을 삽입하거나 조회하는 배치 작업(batch processing)을 통해, 개별적으로 여러 번 처리하는 것보다 더 효율적일 수 있습니다.
👉 예시(Java 데이터베이스 배치 처리)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class BatchProcessingExample {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password")) {
String query = "INSERT INTO students (name, age) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(query);
conn.setAutoCommit(false);
for (int i = 0; i < 100; i++) {
pstmt.setString(1, "Student " + i);
pstmt.setInt(2, 20 + i);
pstmt.addBatch();
}
pstmt.executeBatch();
conn.commit();
System.out.println("Batch insertion completed.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
4️⃣ 요약.
I/O 바운드 작업은 입출력 작업의 속도에 의해 성능이 제한되는 작업을 말합니다.
파일 읽기/쓰기, 네트워크 통신, 데이터베이스 조회 등이 대표적인 I/O 바운드 작업입니다.
이러한 작업은 CPU가 데이터를 처리하는 것보다 입출력 장치에서 데이터를 가져오거나 보내는 시간이 더 오래 걸리는 경우가 많아, CPU는 대기 상태에 머무르게 됩니다.
이를 개선하기 위해 비동기 프로그래밍, 멀티스레딩, 캐싱 등의 기법을 활용하여 입출력 효율을 높이고, 전체 시스템 성능을 향상 시킬 수 있습니다.
-
💾 [CS] 함수형 프로그래밍(Function Programming, FP)이란 무엇일까요?
💾 [CS] 함수형 프로그래밍(Function Programming, FP)이란 무엇일까요?
함수형 프로그래밍(Function Programming, FP)은 함수를 기본 구성 요소로 사용하는 프로그래밍 패러다임입니다.
함수형 프로그래밍(Function Programming, FP)에서는 순수 함수와 데이터의 불변성을 강조하며, 부작용(Side Effect)을 피하는 것을 목표로 합니다.
이를 통해 코드의 가독성, 유지보수성, 재사용성을 높이고, 병렬 처리와 같은 상황에서도 안정적이고 예측 가능한 코드를 작성할 수 있습니다.
1️⃣ 함수형 프로그래밍(Function Programming, FP)의 핵심 개념.
1️⃣ 순수 함수(Pure Function)
순수 함수(Pure Function)는 입력값이 같으면 항상 같은 결과를 반환하는 함수입니다.
함수의 출력이 외부 상태에 의존하지 않고, 함수 내부에서 외부 상태를 변경하지도 않습니다.
순수 함수(Pure Function)는 부작용(Side Effect)이 없기 때문에, 테스트하기 쉽고, 함수의 동작을 예측할 수 있습니다.
👉 예시.
// 순수 함수 예시
public int add(int a, int b) {
return a + b; // 입력값이 같으면 항상 같은 결과를 반환
}
2️⃣ 불변성(Immutability)
함수형 프로그래밍에서는 데이터의 불변성을 중요하게 생각합니다.
즉, 데이터가 생성된 후에는 변경되지 않아야 하며, 필요하면 새로운 데이터를 생성합니다.
불변성은 코드의 안정성을 높이고, 동시성 문제를 피할 수 있게 해줍니다.
👉 예시.
// Java에서의 불변 객체
final String name = "Kobe"; // name은 변경할 수 없는 값
📝 동시성 문제(Concurrency Issue)
여러 작업이 동시에 실행될 때 발생할 수 있는 오류나 비정상적인 동작을 말합니다.
프로그램에서 여러 스레드나 프로세스가 동시에 동일한 데이터나 자원에 접근하고 수정하려고 할 때 동시성 문제(Concurrency Issue)가 발생할 수 있습니다.
특히 데이터의 일관성과 안정성을 유지하는 것이 어려워지며, 이는 프로그램의 버그, 충돌, 예측하지 못한 동작을 초래할 수 있습니다.
📝 스레드(Thread)
프로세스 내에서 실행되는 가장 작은 단위의 작업 흐름을 의미합니다.
프로세스가 메모리, 파일 핸들, 네트워크 연결과 같은 리소스를 포함하는 독립적인 실행 단위라면, 스레드(Thread)는 프로세스 내부에서 실제로 코드가 실행되는 실행 흐름입니다.
하나의 프로세스는 여러 개의 스레드(Thread)를 포함할 수 있으며, 각 스레드(Thread)는 독립적으로 실행될 수 있습니다.
📝 프로세스(Process)
컴퓨터에서 실행 중인 프로그램의 인스턴스를 말합니다.
간단히 말해, 프로그램이 메모리에서 실행될 때 프로세스(Process)가 됩니다.
컴퓨터에서 실행되는 모든 응용 프로그램(예: 웹 브라우저, 텍스트 편집기, 백그라운드 서비스 등)은 각각 하나의 프로세스(Process)입니다.
3️⃣ 고차 함수(Higher-Order Function)
고차 함수(Higher-Order Function)는 다른 함수를 인자로 받거나, 결과로 함수를 반환하는 함수입니다.
이를 통해 함수를 조합하거나, 동적으로 함수를 생성할 수 있으며, 코드의 재사용성을 높일 수 있습니다.
👉 예시.
public static void main(String[] args) {
Function<Integer, Integer> square = X -> X * X;
System.out.println(applyFunction(square, 5)); // 25 출력
}
public static int applyFunction(Function<Integer, Integer> func, int value) {
return func.apply(value);
}
4️⃣ 일급 시민(First-Class Citizen)
함수형 프로그래밍에서는 함수가 일급 시민(First-Class Citizen)으로 취급됩니다.
이는 함수를 변수에 할당하거나, 함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있다는 것을 의미합니다.
함수가 다른 데이터 타입(숫자, 문자열 등)처럼 취급되기 때문에 동적으로 함수의 행동을 변경할 수 있습니다.
📝 일급 시민(First-Class Citizen, 또는 First-Class Object)
프로그래밍 언어에서 변수나 함수가 다른 데이터 타입과 동일하게 취급되고, 자유롭게 사용할 수 있는 객체를 의미합니다.
일급 시민(First-Class Citizen 또는 First-Class Object)으로 간주되는 개체는 변수에 할당되거나, 함수의 인자(Arguments)로 전달되거나, 함수의 반환 값으로 사용할 수 있는 등 다양한 방식으로 활용될 수 있습니다.
이 개념은 주로 함수형 프로그래밍(Function Programming, FP)에서 사용되며, 특히 함수를 일급 시민(First-Class Citizen 또는 First-Class Object)으로 취급할 수 있는지 여부가 그 언어의 함수형 프로그래밍(Function Programming, FP) 지원 수준을 결정하는 중요한 요소가 됩니다.
5️⃣ 부작용(Side Effects) 없는 코드.
함수형 프로그래밍(Function Programming, FP)에서는 함수가 부작용(Side Effect)을 일으키지 않도록 작성합니다.
부작용(Side Effects)이란 함수가 외부 상태를 변경하거나, 입출력 작업을 수행하는 것을 말합니다.
부작용(Side Effects)이 없기 때문에, 코드의 예측 가능성과 재사용성이 높아집니다.
6️⃣ 함수 합성(Function Composition)
함수 합성(Function Composition)은 여러 함수를 결합하여 새로운 함수를 만드는 것을 말합니다.
작은 함수들을 조합하여 복잡한 연산을 처리할 수 있게 합니다.
이는 재사용 가능한 작은 함수를 만들어, 더 큰 기능을 구현할 때 유용하게 사용할 수 있습니다.
2️⃣ 함수형 프로그래밍(Function Programming, FP)의 특징.
1️⃣ 코드의 가독성과 유지보수성.
함수형 프로그래밍(Function Programming, FP)에서는 짧고 간결한 코드를 작성할 수 있으며, 코드가 명확하고 읽기 쉬워집니다.
각 함수가 독립적으로 동작하기 때문에, 모듈화(Modularization)와 테스트가 용이합니다.
🙋♂️ 모듈화(Modularization)란 무엇일까요?
2️⃣ 병렬 처리와 동시성.
함수형 프로그래밍(Function Programmin, FP)의 불변성 덕분에, 여러 스레드(Thread)에서 동시 실행하더라도 데이터의 일관성이 유지됩니다.
따라서 병렬 처리를 할 때 데이터 경합(Race Condition) 문제가 발생하지 않으며, 안전하게 실행할 수 있습니다.
📝 데이터 경합 또는 레이스 컨디션(Race Condition).
두 개 이상의 스레드(Thread) 또는 프로세스(Process)가 동시에 공유 자원에 접근하고, 그 결과가 실행 순서에 따라 달라지는 상황을 말합니다.
즉, 동시성 프로그래밍(Concurrent Programming)에서 스레드들이 경쟁적으로 자원에 접근할 때 발생하는 문제로, 프로그램의 의도치 않은 결과를 초래할 수 있습니다.
레이스 컨디션(Race Condition)은 특히 공유 자원(변수, 데이터 구조, 파일 등)에 대해 읽기와 쓰기 작업이 동시에 이루어지는 경우에 발생하며, 동기화가 제대로 이루어지지 않으면 데이터의 일관성이 깨지게 됩니다.
3️⃣ 테스트 용이성.
순수 함수(Pure Function)는 입력값과 출력값의 관계가 명확하기 때문에 테스트하기 쉽습니다.
또한, 부작용이 없기 때문에 독립적으로 테스트할 수 있어 단위 테스트(Unit Testing)에 적합합니다.
📝 단위 테스트(Unit Testing)
소프트웨어의 개별 구성 요소(함수, 메서드, 클래스 등)가 올바르게 동작하는지 검증하는 테스트 방법입니다.
단위 테스트(Unit Testing)의 목적은 프로그램의 가장 작은 단위(단위, 유닛)가 예상대로 작동하는지 확인하는 것입니다.
이를 통해 각 구성 요소가 독립적으로 정확하게 동작하는지를 보장할 수 있습니다.
3️⃣ 함수형 프로그래밍의 예시 (Java)
Java 8부터는 함수형 프로그래밍(Function Programmig, FP) 스타일을 지원하기 위해 람다 표현식(Lambda Expression)과 스트림(Stream) API를 도입했습니다.
📝 람다 표현식(Lambda Expression)
익명 함수(Anonymous Function)로, 이름이 없는 간단한 함수를 의미합니다.
람다 표현식(Lambda Expression)을 사용하면 함수를 보다 간결하고 직관적으로 정의할 수 있으며, 코드의 가독성을 높이고 간단한 작업을 수행하는 데 유용합니다.
람다 표현식(Lambda Function)은 주로 함수형 프로그래밍(Function Programming, FP) 스타일을 지원하기 위해 도입되었으며, Java 8부처 Java에서도 사용할 수 있게 되었습니다.
🙋♂️ Java Stream이란 무엇일까요?
1️⃣ 람다 표현식(Lambda Expression)
람다 표현식(Lambda Expression)을 사용하면 간결하게 함수를 정의할 수 있습니다.
// 기존 방식
public int add(int a, int b) {
return a + b;
}
// 람다 표현식
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(3,4)); // 7 출력
2️⃣ 스트림(Stream) API
스트림은 컬렉션의 데이터를 필터링, 변환, 정렬, 집계할 수 있는 함수형 프로그래밍 스타일의 API입니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Integer> nnumbers = Arrays.asList(1, 2, 3, 4, 5);
// 짝수만 필터링하고, 각 숫자에 2를 곱한 리스트 생성
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(result); // [4, 8] 출력
}
}
4️⃣ 함수형 프로그래밍의 장점.
1️⃣ 코드의 간결성.
함수형 프로그래밍에서는 코드가 짧고 직관적이기 때문에, 읽기 쉽고 유지보수가 용이합니다.
2️⃣ 재사용성과 확장성.
작고 재사용 가능한 함수를 작성하여, 다른 프로그램이나 프로젝트에서 재사용할 수 있습니다.
3️⃣ 병렬 처리의 용이성.
불변성 덕분에 병렬 처리 환경에서도 안전하게 실행할 수 있으며, 효율적인 병렬 처리가 가능합니다.
4️⃣ 디버깅과 테스트의 용이성
순수 함수는 독립적으로 테스트할 수 있어, 디버깅과 단위 테스트가 쉽습니다.
5️⃣ 함수형 프로그래밍의 단점.
1️⃣ 러닝 커브.
함수형 프로그래밍의 개념에 익숙하지 않은 개발자에게는 러닝 커브가 있을 수 있습니다.
특히 람다 표현식(Lambda Expression)과 고차 함수(Higher-Order Function)에 익숙해지는 데 시간이 필요할 수 있습니다.
2️⃣ 퍼포먼스 이슈.
불변성 때문에 새로운 객체를 매번 생성해야 하는 경우 메모리 사용량이 증가할 수 있으며, 성능에 영향을 미칠 수 있습니다.
따라서 상황에 따라 성능 최적화를 고려해야 합니다.
6️⃣ 요약.
함수형 프로그래밍은 순수 함수, 불변성, 부작용 없는 코드를 강조하여, 가독성과 유지보수성, 재사용성이 높은 코드를 작성하는 프로그래밍 패러다임입니다.
Java 8 이후의 람다 표현식(Lambda Expression)과 스트림(Stream) API는 함수형 프로그래밍(Function Programming ,FP) 스타일을 도입하여, 개발자가 더욱 간결하고 효율적인 코드를 작성할 수 있게 해줍니다.
함수형 프로그래밍은 병렬 처리와 동시성 문제에 강점을 가지며, 작은 함수들을 조합하여 모듈화(Modularization)된 코드를 작성할 수 있어, 코드의 재사용성과 확장성을 높이는 데 유리합니다.
-
🍃[Spring] Spring Boot를 사용하여 HTML, CSS, JS로 만든 Frontend 페이지를 로컬 서버와 연결하는 방법은 무엇일까요?
🍃[Spring] Spring Boot를 사용하여 HTML, CSS, JS로 만든 Frontend 페이지를 로컬 서버와 연결하는 방법은 무엇일까요?
Spring Boot를 사용하여 HTML, CSS, JavaScript로 만든 프런트엔드 페이지를 로컬 서버와 연결하려면 다음과 같은 단계가 필요합니다.
이 과정에서는 정적 리소스를 Spring Boot 프로젝트에 포함하고, 컨트롤러를 통해 요청을 처리하여 정적페이지를 제공하는 방법을 설명합니다.
1️⃣ Spring Boot 프로젝트에 정적 파일 추가.
HTML, CSS, JavaScript 파일은 Spring Boot 프로젝트의 정적 리소스 디렉토리에 배치합니다.
👉 프로젝트 디렉토리 구조.
src
└── main
├── java
│ └── com.example.demo (패키지 구조)
│ └── DemoApplication.java
├── resources
│ ├── static
│ │ ├── css
│ │ │ └── styles.css
│ │ ├── js
│ │ │ ├── scripts.js
│ │ │ └── login-scripts.js
│ │ ├── signup.html
│ │ └── login.html
│ └── templates (필요한 경우 Thymeleaf 템플릿 파일)
└── application.properties
static 디렉토리는 기본적으로 정적 리소스를 제공하는 역할을 하며, 여기서 CSS, JavaScript, 이미지 및 HTML 파일을 로드할 수 있습니다.
HTML 파일(signup.html, login.html)은 직접 static 디렉토리에 배치하여 서버가 요청을 받으면 직접 로드되도록 합니다.
2️⃣ Spring Boot 컨트롤러 설정.
Spring Boot 에서 /login과 /signup 요청을 처리하도록 간단한 컨트롤러를 만듭니다.
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AuthController {
@GetMapping("/login")
public String loginPage() {
return "login.html"; // static/login.html 파일을 반환
}
@GetMapping("/signup")
public String signupPage() {
return "signup.html"; // static/signup.html 파일을 반환
}
}
3️⃣ 정적 파일 및 자원 접근
HTML 파일에서 CSS 및 JavaScript 파일에 대한 경로를 수정하여 Spring Boot에서 제공하는 정적 리소스를 제대로 불러올 수 있게합니다.
👉 login.html(예시)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<link rel="stylesheet" href="/css/styles.css"> <!-- 절대 경로로 수정 -->
</head>
<body>
<div class="login-container">
<form id="loginForm" class="login-form">
<h2>Login</h2>
<div class="input-group">
<label for="login-email">Email</label>
<input type="email" id="login-email" name="login-email" required>
</div>
<div class="input-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" name="login-password" required>
</div>
<button type="submit">Login</button>
<p class="signup-link">Don't have an account? <a href="/signup">Sign up here</a></p> <!-- Spring Boot 경로 사용 -->
</form>
</div>
<script src="/js/login-scripts.js"></script> <!-- 절대 경로로 수정 -->
</body>
</html>
4️⃣ Spring Boot 애플리케이션 실행
이제 Spring Boot 애플리케이션을 실행하면 로컬 서버에서 /login 및 /signup 경로로 접근하여 프런트엔드 페이지를 볼 수 있습니다.
👉 애플리게이션 실행 명령어
./gradlwe bootRun #Gradle 사용시
mvn spring-boot:run # Maven 사용시
5️⃣ 추가 설정(필요한 경우)
1️⃣ CORS 설정
프런트엔드 페이지에서 다른 서버의 API에 접근하려면 CORS 설정이 필요할 수 있습니다.
WebMvcConfigurer를 사용하여 다음과 같이 설정할 수 있습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}
📝 CORS(Cross-Origin Resource Sharing)
웹 브라우저에서 실행되는 웹 애플리케이션이 다른 도메인에서 호스팅 되는 리소스에 접근할 수 있도록 하는 보안 메커니즘입니다.
기본적으로 웹 브라우저는 보안상의 이유로, 한 도메인에서 로드된 웹 페이지가 다른 도메인에 요청을 보내는 것을 제한합니다.
이를 “동일 출처 정책(Same-Origin-Policy)”라고 합니다.
2️⃣ API와 연결.
로그인, 회원가입 등의 기능을 구현하기 위해 백엔드 API를 작성할 수 있습니다.
예를 들어, /login API를 만들어 JavaScript에서 해당 API로 AJAX 요청을 보낼 수 있습니다.
📝 AJAX(Asynchronous JavaScript and XML)
웹 페이지를 새로고침하지 않고 비동기 방식으로 서버와 데이터를 주고받을 수 있게 해주는 기술을 말합니다.
웹 페이지를 동적으로 업데이트 할 수 있기 때문에 사용자가 더 빠르고 직관적인 경험을 할 수 있도록 도와줍니다.
6️⃣ 요약.
Spring Boot 프로젝트의 static 디렉토리에 HTML, CSS, JavaScript 파일을 넣어 정적 리소스를 제공합니다.
컨트롤러를 통해 /login 및 /signup 경로를 설정하고 HTML 파일을 로드합니다.
JavaScript 코드는 프런트엔드에서 필요한 동작을 처리하며, 추가로 API를 호출할 수 있습니다.
-
💾 [CS] 모듈화(Modularization)란 무엇일까요?
💾 [CS] 모듈화(Modularization)란 무엇일까요?
모듈화(Modularization)는 소프트웨어 개발에서 프로그램을 독립적이고 재사용 가능한 작은 단위(모듈)로 나누는 설계 기법입니다.
각 모듈은 특정 기능을 수행하며, 다른 모듈과 명확하게 정의된 인터페이스를 통해 상호작용 합니다.
모듈화의 목적은 코드의 가독성, 유지보수성, 재사용성을 높이고, 복잡한 시스템을 더 쉽게 관리할 수 있도록 하는 것 입니다.
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
🙋♂️ 소프트웨어 공학에서의 모듈.
🙋♂️ API에서의 인터페이스와 소프트웨어 공학에서의 인터페이스 개념.
1️⃣ 모듈화의 개념.
1️⃣ 모듈(Module).
모듈은 자기완결적인 독립적 코드 단위로, 특정 기능을 수행하는 코드의 집합입니다.
각 모듈은 하나의 기능이나 작업에 집중하며, 다른 모듈과의 의존성을 최소화하도록 설계됩니다.
예를 들어, 로그인 처리, 데이터베이스 접근, 파일 입출력, 사용자 인터페이스 등과 같은 각각의 기능을 담당하는 모듈이 있을 수 있습니다.
2️⃣ 인터페이스(Interface).
모듈화(Modularization)에서 중요한 개념은 명확한 인터페이스를 정의하는 것입니다.
인터페이스(Interface)는 모듈(Module)이 외부에서 제공하는 기능을 정의하며, 다른 모듈이 기능을 호출할 때 사용할 수 있는 명세입니다.
인터페이스(Interface) 덕분에 모듈 내부의 구현이 변경되더라도, 외부 모듈과의 연결 방식은 변하지 않으므로 유연한 코드 수정이 가능합니다.
2️⃣ 모듈화의 장점.
1️⃣ 유지보수성 향상.
모듈화된 프로그램은 개별 모듈을 독립적으로 수정할 수 있기 때문에, 특정 기능에 문제가 발생해도 해당 모듈만 수정하면 됩니다.
전체 시스템에 영향을 주지 않고 변경이 가능하므로 유지보수가 용이합니다.
2️⃣ 재사용성 증가.
모듈은 독립적인 기능 단위이기 때문에, 여러 프로그램에서 재사용할 수 있습니다.
예를 들어, 로그인 처리 모듈을 별도로 만들어 놓으면, 다른 프로젝트에서도 동일한 모듈을 재사용할 수 있어 개발 시간을 줄일 수 있습니다.
3️⃣ 가독성 및 코드 관리.
모듈화(Modularization)는 프로그램의 구조를 체계적으로 관리할 수 있게 하며, 각각의 모듈이 명확한 기능을 담당하기 때문에 코드의 가독성이 높아집니다.
이는 큰 프로젝트나 복잡한 시스템에서 특히 유용하며, 코드를 이해하기 쉽고 디버깅하기도 용이합니다.
4️⃣ 개발 팀 협업.
모듈화를 통해 개발 팀이 작업을 나눌 수 있습니다.
각 개발자는 다른 모듈에 영향을 주기 않고 자신의 모듈을 개발할 수 있기 때문에 병렬 개발이 가능해집니다.
📝 병렬 개발(Parallel Development)
여러 개발자가 동시에 동일한 프로젝트에 다른 기능이나 모듈(Module)을 개발하는 소프트웨어 개발 방식을 의미합니다.
병렬 개발(Parallel Development)을 통해 프로젝트의 개발 속도를 높이고, 기능을 더 빠르게 구현할 수 있습니다.
대규모 프로젝트에서 흔히 사용되는 개발 방식으로, 개발 효율성을 극대화할 수 있는 장점이 있습니다.
이는 큰 프로젝트나 개발 속도를 높이고, 효율적인 협업을 가능하게 합니다.
5️⃣ 유연성과 확장성.
모듈화(Modularization)를 통해 프로그램을 쉽게 확장할 수 있습니다.
새로운 기능이 필요할 때, 기존 모듈(Module)을 수정하거나 새로운 모듈(Module)을 추가하는 방식으로 쉽게 확장할 수 있습니다.
시스템의 유연성이 증가하여 다양한 요구 사항에 맞개 프로그램을 조정할 수 있습니다.
3️⃣ 모듈화의 예.
1️⃣ Java에서의 모듈화.
Java 9부터는 모듈 시스템을 도입하여, 애플리케이션을 명확한 의존성을 가진 여러 모듈로 나눌 수 있게 되었습니다.
예를 들어, module-info.java 파일을 사용해 모듈을 정의하고, 해당 모듈이 어떤 모듈을 사용하는지, 외부에 어떤 패키지를 공개하는지 명확히 할 수 있습니다.
module com.example.mymodule {
export com.example.service; // 외부에 공개할 패키지
requires java.sql; // 필요한 모듈
}
2️⃣ 프로젝트 구조의 모듈화.
웹 애플리케이션에서 일반적으로 다음과 같은 모듈로 나눌 수 있습니다.
데이터 접근 모듈 : 데이터베이스와 상호작용하는 기능을 담당.
비즈니스 로직 모듈 : 애플리케이션의 핵심 로직을 처리.
프레젠테이션 모듈 : 사용자 인터페이스 및 요청/응답 처리.
유틸리티 모듈 : 다양한 모듈에서 공통적으로 사용하는 기능을 제공.
이러한 모듈 구조를 통해 개발자는 각 모듈을 독립적으로 개발, 테스트, 유지보수할 수 있으며, 각 모듈은 하나의 기능에 집중하도록 설계됩니다.
4️⃣ 요약.
모듈화(Modularization)는 프로그램을 작고 독립적인 모듈로 나누어 가독성, 유지보수성, 재사용성을 높이는 설계 기법입니다.
모듈화(Modularization)된 프로그램은 기능별로 독립적인 모듈(Module)로 구성되어, 각 모듈이 특정 작업을 수행하고, 다른 모듈과 명확한 인터페이스(Interface)로 상호작용합니다.
이를 통해 개발, 유지보수, 확장이 쉬워지고, 팀 협업도 원활해질 수 있습니다.
모듈화(Modularization)를 잘 설계하면 복잡한 시스템을 체계적으로 관리할 수 있으며, 코드의 재사용성을 높여 개발 속도를 향상시킬 수 있습니다.
-
💾 [CS] 가비지 컬렉션(Garbage Collection)이란 무엇일까요?
💾 [CS] 가비지 컬렉션(Garbage Collection)이란 무엇일까요?
가비지 컬렉션(Garbage Collection)은 더 이상 사용하지 않는 객체를 메모리에서 자동으로 해제하는 메모리 관리 기법입니다.
자바(Java)와 같은 프로그래밍 언어에서, 개발자가 수동으로 메모리를 해제하지 않아도 가비지 컬렉터(Garbage Collector)가 자동으로 불필요한 객체를 감지하고 메모리를 회수하여 메모리 누수(Memory Leak)를 방지합니다.
1️⃣ 가비지 컬렉션의 필요성.
1️⃣ 자동 메모리 관리.
가비지 컬렉션(Garbage Collection)은 프로그래머가 메모리 할당과 해제를 직접 관리할 필요 없이, 힙 메모리(Heap Memory)에서 더 이상 참조되지 않는 객체를 자동으로 제거하여 메모리 관리를 간편하게 합니다.
자바(Java)와 같은 언어에서는 new 키워드로 객체를 생성하고 나면, 메모리 해제를 가비지 컬렉터(Garbage Collector)에 맡기게 됩니다.
2️⃣ 메모리 누수 방지.
가비지 컬렉션(Garbage Collection)은 더 이상 필요하지 않은 객체가 메모리에서 해제되지 않고 계속 남아 있어 메모리를 차지하는 상황, 즉 메모리 누수(Memory Leak)를 방지합니다.
이를 통해 효율적인 메모리 사용을 보장합니다.
2️⃣ 가비지 컬렉션(Garbage Collection)의 기본 원리.
1️⃣ 객체의 수명 주기.
자바 프로그램이 실행되면서, 객체는 힙 메모리(Heap Memory)에 할당됩니다.
어떤 객체가 생성된 후, 해당 객체를 참조하는 변수나 다른 객체가 없으면, 그 객체는 더 이상 사용되지 않는 “쓰레기(Garbage)”가 됩니다.
가비지 컬렉터(Garbage Collector)는 주기적으로 힙 메모리(Heap Memory)를 스캔하여 더 이상 참조되지 않는 객체를 찾아 메모리에서 제거 합니다.
2️⃣ 참조의 개념.
객체는 참조 변수를 통해 접근할 수 있으며, 가비지 컬렉터(Garbage Collector)는 객체가 다른 객체나 변수로부터 참조되고 있는지를 판단해 가비지(Garbage) 여부를 결정합니다.
참조되지 않는 객체는 더 이상 접근할 수 없으므로, 가비지 컬렉터(Garbage Collector)가 메모리에서 제거할 수 있습니다.
3️⃣ 가비지 컬렉션(Garbage Collection)의 동작 방식.
가비지 컬렉션(Garbage Collection)의 구체적인 동작 방식은 사용되는 알고리즘에 따라 다릅니다.
자바에서는 다양한 알고리즘이 가비지 컬렉션(Garbage Collection)을 위해 사용되며, 그중 대표적인 몇 가지 방법을 설명하겠습니다.
1️⃣ 마크-앤-스윕(Mark-and-Sweep) 알고리즘.
마크 단계(Mark Phase)
프로그램이 실행되는 동안 참조 가는한 객체를 “마킹” 합니다.
참조되지 않는 객체는 마킹되지 않은 채로 남습니다.
스윕 단계(Sweep Phase)
마킹이 되지 않은 객체를 힙 메모리(Heap Memory)에서 제거하여 메모리를 회수합니다.
이 과정에서 힙 메모리 조각화(Fragmentation)가 발생할 수 있습니다.
2️⃣ 복사(Copying) 알고리즘
힙 메모리(Heap Memory) 두 개의 영역(From-Space와 To-Space)으로 나눕니다.
참조 가능한 객체를 To-Space로 복사하고, From-Space에 남은 쓰레기 객체는 삭제합니다.
복사 과정에서 메모리 조각화(Memory Fragmentation) 문제를 해결할 수 있지만, 메모리를 두 개의 영역으로 나누어야 한다는 단점이 있습니다.
📝 메모리 조각화(Memory Fragmentation)
프로그램이 메모리를 할당하고 해제하는 과정에서 메모리 공간이 쪼개져 효율적으로 사용할 수 없는 상태를 말합니다.
이는 메모리의 사용 가능한 공간이 충분히 존재함에도 불구하고, 연속적인 큰 메모리를 할당하지 못하는 상황을 초래할 수 있습니다.
메모리 조각화(Memory Fragmentation)는 시스템의 성능 저하와 메모리 낭비의 원인이 되며, 특히 동적 메모리 할당을 많이 사용하는 프로그램에서 자주 발생합니다.
3️⃣ 세대별(Generational) 가비지 컬렉션.
자바의 HotSpot JVM에서 사용되는 가비지 컬렉션 방식으로, 객체의 수명을 기반으로 메모리를 관리합니다.
힙 메모리(Heap Memory)를 Young Generation, Old Generation, Permanent Generation으로 나누어, 객체의 수명에 따라 효율적으로 관리합니다.
Young Generation
새로 생성된 객체가 저장되는 공간으로, 대부분의 객체는 여기에 생성되며 빠르게 소멸합니다.
Minor GC가 주기적으로 발생하여 메모리를 해제합니다.
Old Generation
Young Generation을 거쳐 오래 살아남은 객체가 이동하는 공간으로, Major GC가 발생하여 메모리를 해제합니다.
Permanent Generation(Metaspace)
클래스 정보, 메서드, 상수 풀 등 JVM에 필요한 메타데이터를 저장하는 공간입니다.
Java 8 부터는 Metaspace라는 새로운 형태로 관리됩니다.
📝 메타데이터(Metadata)
데이터를 설명하는 데이터를 의미합니다.
즉, 어떤 데이터에 대한 정보나 속성을 제공하는 데이터로, 원본 데이터의 내용, 구조, 형식, 속성 등을 설명하고 정의하는 역할을 합니다.
메타데이터는 데이터를 더 잘 이해하고, 찾고, 관리할 수 있도록 도와줍니다.
4️⃣ 자바의 가비지 컬렉션 과정.
1️⃣ Minor GC
Young Generation에서 발생하는 가비지 컬렉션입니다.
새로운 객체가 생성되다가 Young Generation이 가득 차게 되면 Minor GC가 실행되어 참조되지 않는 객체를 제거합니다.
Minor GC는 빠르고 자주 실행됩니다.
2️⃣ Major GC(Full GC)
Old Generation에서 발생하는 가비지 컬렉션입니다.
Young Generation을 거쳐 오래 살아남은 객체가 Old Generation으로 이동하게 되며, Old Generation이 가득 차면 Major GC가 실행됩니다.
Major GC는 Minor GC에 비해 느리고, 프로그램의 일시적인 중단(Stop-the-World)을 유발할 수 있습니다.
5️⃣ 가비지 컬렉션의 장단점.
1️⃣ 장점.
자동 메모리 관리
개발자가 메모리 관리를 수동으로 하자 않아도 되어 프로그래밍이 간편해지고, 메모리 누수(Memory Leak)을 방지할 수 있습니다.
효율적인 메모리 사용
가비지 컬렉터(Garbage Collector)는 불필요한 객체를 자동으로 회수하여 메모리 사용을 최적화합니다.
2️⃣ 단점.
성능 문제
가비지 컬렉션(Garbage Collection)이 실행될 때 프로그램이 일시적으로 멈출 수 있는 “Stop-the-World” 현상이 발생할 수 있습니다.
예측 불가능한 실행 시점
가비지 컬렉션(Garbage Collection)은 특정 시점에 예측 불가능하게 실행되므로, 실시간 애플리케이션에서는 성능에 영향을 줄 수 있습니다.
6️⃣ 가비지 컬렉션의 최적화 방법.
1️⃣ JVM 옵션 설정.
-Xms, -Xmx, -Xmn 등을 사용하여 힙 메모리 크기를 적절히 설정함으로써 가비지 컬렉션의 빈도를 조절할 수 있습니다.
예를 들어, -Xms512m -Xmx1024m 옵션은 JVM이 시작할 때 512MB의 힙 메모리(Heap Memory)를 사용하고, 최대 1024MB까지 사용할 수 있도록 설정합니다.
2️⃣ GC 알고리즘 선택.
JVM은 다양한 가비지 컬렉터 알고리즘을 지원하며, 프로그램의 특성에 맞는 알고리즘을 선택하면 성능을 최적화할 수 있습니다.
예: Serial GC, Paralle GC, G1 GC 등. G1 GC는 Stop-the-World 현상을 최소화하는 데 유리합니다.
3️⃣ 메모리 누수 예방.
가비지 컬렉터(Garbage Collector)가 메모리를 자동으로 관리하지만, 명시적으로 해제할 수 없는 리소스(파일 핸들, 데이터 베이스 연결 등)는 코드에서 직접 관리해야 합니다.
전역 변수나 static 객체로 인한 메모리 누수(Memory Leak)를 주의해야 합니다.
이러한 객체가 참조를 유지하고 있으면 가비지 컬렉터(Garbage Collector)는 메모리를 해제하지 않습니다.
7️⃣ 요약.
가비지 컬렉션(Garbage Collection)은 자바와 같은 프로그래밍 언어에서 사용되지 않은 객체를 자동으로 메모리에서 해제하여 메모리 누수(Memory Leak)를 방지하는 메모리 관리 기법입니다.
가비지 컬렉터(Garbage Collector)는 주기적으로 실행되어, 참조되지 않는 객체를 감지하고 메모리를 회수합니다.
자바에서는 세대별 가비지 컬렉션(Garbage Collection)을 통해 객체의 수명에 따라 메모리를 효율적으로 관리합니다.
가비지 컬렉션 덕분에 개발자는 메모리 관리에 대한 부담을 덜고, 더 안정적이고 효율적인 프로그램을 작성할 수 있습니다.
-
💾 [CS] 객체지향 설계(Object-Oriented Design, OOD)란 무엇일까요?
💾 [CS] 객체지향 설계(Object-Oriented Design, OOD)란 무엇일까요?
객체지향 설계(Object-Oriented Design, OOD)는 객체지향 프로그래밍(Object-Oriented Programming, OOP)의 원칙과 개념을 바탕으로, 프로그램을 객체라는 독립적인 단위로 설계하는 방법을 의미합니다.
객체지향 설계(Object-Oriented Design, OOD)는 프로그램의 구성 요소를 클래스와 객체로 나누고, 이들 간의 관계를 정의하여 효율적이고 재사용 가능하며 유지보수하기 쉬운 소프트웨어를 만드는 것을 목표로 합니다.
🙋♂️ 객체지향 설계(Object-Oriented Design, OOD)란 무엇일까요?
1️⃣ 객체지향 설계의 핵심 개념.
1️⃣ 객체(Object)
객체는 데이터(속성, 상태)와 이 데이터를 조작하는 메서드(동작)가 결합된 독립적인 단위입니다.
현실 세계의 사물이나 개념을 모델링하여 프로그램 내에서 사용할 수 있는 형태로 만들기 때문에, 직관적으로 이해하고 설계할 수 있습니다.
2️⃣ 클래스(Class)
클래스는 객체를 생성하기 위한 청사진 또는 설계도입니다.
객체가 가지는 속성과 메서드를 정의합니다.
클래스는 객체의 특성(속성)과 행동(메서드)을 정의하여, 같은 형태의 객체를 다수 생성할 수 있게 합니다.
3️⃣ 객체 간의 관계.
객체지향 설계(Object-Oriented Design, OOD)에서는 객체 간의 관계를 정의하는 것이 중요합니다.
이러한 관계는 다음과 같이 나뉩니다.
연관 관계(Association)
집합 관계(Aggregation)
구성 관계(Composition)
상속 관계(Inheritance)
1️⃣ 연관 관계(Association)
두 객체가 서로 관련된 관계입니다.
예: 학생 객체와 수업 객체가 서로 연결될 수 있음.
2️⃣ 집합 관계(Aggregation)
객체가 다른 객체의 일부로 포함되는 관계이지만, 부분 객체는 독립적으로 존재할 수 있습니다.
예: 팀과 팀원.
3️⃣ 구성 관계(Composition)
부분 객체가 전체 객체에 완전히 소속되어 있으며, 전체 객체가 사라지면 부분 객체도 함께 사라집니다.
예: 집과 방.
4️⃣ 상속 관계(Inheritance)
객체가 다른 객체의 특성을 상속받는 관계입니다.
예: 동물 클래스가 있고, 이를 상속받아 개, 고양이 클래스를 정의.
2️⃣ 객체지향 설계의 원칙(SOLID)
객체지향 설계(Object-Oriented Design, OOD)에서는 SOLID 원칙이 자주 언급되며, 이는 유연하고 유지보수하기 좋은 소프트웨어 설계를 위한 중요한 지침입니다.
🙋♂️ SOLID 원칙.
1️⃣ 단일 책임 원칙(Single Responsibility Principle, SRP)
클래스는 하나의 책임만 가져야 하며, 그 책임을 완벽하게 수행하도록 설계해야 합니다.
즉, 클래스가 변경되는 이유는 하나의 이유여야 합니다.
예: Order 클래스가 주문 처리와 주문 데이터 저장을 모두 수행하는 것이 아니라, 주문 처리 로직을 담당하는 Order 클래스와 주문 데이터를 저장하는 OrderRespository 클래스로 나누는 것이 좋습니다.
2️⃣ 개방-폐쇄 원칙(Open-Closed Principle, OCP)
클래스는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
즉, 기존 코드를 수정하지 않고도 기능을 추가할 수 있어야 합니다.
예: 새로운 기능을 추가할 때 기존 클래스의 코드를 수정하지 않고도 상속이나 인터페이스 구현을 통해 기능을 확장할 수 있어야 합니다.
3️⃣ 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
서브 클래스는 언제나 기반 클래스로 대체할 수 있어야 합니다.
즉, 상위 클래스의 객체를 사용하는 코드에서 하위 클래스의 객체를 사용할 수 있어야 하며, 프로그램이 정상적으로 동작해야 합니다.
예: Bird 클래스의 하위 클래스인 Penguin이 fly() 메서드를 구현하지만, 현실에서는 펭귄이 날지 못하기 때문에 이 원칙을 위반할 수 있습니다. 이러한 경우 설계를 다시 고려해야 합니다.
4️⃣ 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
구현하지 않는 메서드가 있는 인터페이스를 사용하지 않도록, 작고 구체적인 인터페이스로 나누어야 합니다.
하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스를 설계하여, 필요한 기능만 구현하도록 합니다.
예: Printer 인터페이스가 print(), scan(), fax()를 포함하고 있을 때, 프린터만 필요한 경우에도 스캐너와 팩스 기능을 구현해야 한다면, Printable, Scannable, Faxable과 같이 인터페이스를 나누는 것이 좋습니다.
5️⃣ 의존 역전 원칙(Dependency Inversion Principle, DIP)
상위 모듈은 하위 모듈에 의존해서는 안되며, 둘 다 추상화에 의존해야 합니다.
구체적인 클래스가 아닌, 추상화된 인터페이스에 의존하도록 설계하여 유연한 코드를 작성할 수 있습니다.
예: Service 클래스가 Repository 인터페이스를 사용하고, 실제 데이터 저장 로직은 FileRepository나 DatabaseRepository 같은 클래스에서 구현할 수 있도록 설계합니다.
3️⃣ 객체지향 설계의 장점.
1️⃣ 재사용성.
객체지향 설계(Object-Oriented Design, OOD)의 모듈성 덕분에, 하나의 객체를 다양한 프로그램에서 재사용할 수 있습니다.
이는 코드를 재사용 가능하게 하여 개발 시간을 줄이고 비용을 절감할 수 있습니다.
2️⃣ 유지보수성.
각 객체가 독립적이기 때문에, 문제가 발생했을 때 해당 객체만 수정하면 됩니다.
다른 객체에 미치는 영향을 최소화할 수 있어, 유지보수가 용이합니다.
3️⃣ 확장성.
객체지향 설계(Object-Oriented Design, OOD)는 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있도록 유연한 구조를 제공합니다.
새로운 클래스나 메서드를 추가함으로써 기능을 확장할 수 있어, 변화하는 요구 사항에 쉽게 대응할 수 있습니다.
4️⃣ 캡슐화.
객체지향 설계(Object-Oriented Design, OOD)는 캡슐화(Encapsulation)를 통해 객체 내부 상태를 보호하고, 외부에서 접근할 수 있는 방법을 제한합니다.
이는 데이터의 무결성을 유지하고, 코드의 복잡성을 줄이는 데 도움이 됩니다.
📝 데이터의 무결성(Data Integrity)
데이터가 정확하고 일관되며 신뢰할 수 있는 상태를 유지하는 것을 의미합니다.
이는 데이터의 정확성, 일관성, 유효성을 보장하기 위한 개념으로, 데이터가 저장, 처리, 전송되는 동안 손상, 변조, 유실 등이 발생하지 않도록 하는 것을 목표로 합니다.
데이터 무결성(Data Integrity)은 데이터베이스 시스템을 비롯한 다양한 소프트웨어 시스템에서 매우 중요한 개념으로, 데이터의 품질을 유지하는 핵심 요소입니다.
4️⃣ 객체지향 설계의 예.
1️⃣ Car 클래스 설계 예.
public class Car {
private Engine engine; // 캡슐화된 엔진 객체
public Car(Engine engine) { // 의존성 주입
this.engine = engine;
}
public void start() {
engine.start();
}
}
위 설계에서는 Car 클래스가 Engine 객체를 사용하지만, Engine 클래스의 구체적인 구현에 의존하지 않고, Engine 인터페이스를 통해 동작합니다.
만약 전기 엔진이나 가솔린 엔진을 사용하고 싶다면, Engine 인터페이스를 구현한 새로운 클래스만 추가하면 됩니다.
이는 의존 역전 원칙(Dependency Inversion Principle, DIP)을 적용한 예입니다.
5️⃣ 요약.
객체지향 설계(Object-Oriented Design, OOD)는 객체와 클래스를 중심으로 프로그램을 설계하여, 재사용성과 유지보수성이 높은 소프트웨어를 만드는 방법입니다.
객체지향 설계(Object-Oriented Design, OOD)는 SOLID 원칙을 통해 효율적이고 유연한 시스템 구조를 제공하며, 캡슐화, 상속, 다형성, 추상화와 같은 객체지향 프로그래밍의 개념을 적극 활용합니다.
이를 통한 복잡한 시스템을 더 쉽게 이해하고 관리할 수 있으며, 변화하는 요구 사항에도 유연하게 대응할 수 있습니다.
-
💾 [CS] 스택 트레이스(Stack Trace)란 무엇일까요?
💾 [CS] 스택 트레이스(Stack Trace)란 무엇일까요?
스택 트레이스(Stack Trace)는 프로그램 실행 중 예외(Exception)가 발생했을 때, 예외가 발생한 지점과 그 예외로 이어진 함수 호출 경로를 보여주는 디버깅 정보입니다.
이를 통해 개발자는 어떤 예외가 어디서 발생했는지, 그리고 그 예외가 어떻게 발생했는지를 파악할 수 있습니다.
1️⃣ 스택 트레이스(Stack Trace)의 역할.
1️⃣ 디버깅.
스택 트레이스(Stack Trace)를 통해 예외가 발생한 위치를 정확히 찾아내고, 문제를 신속하게 해결할 수 있습니다.
2️⃣ 문제의 원인 파악.
스택 트레이스(Stack Trace)는 예외가 발생하기 전까지 어떤 함수가 호출되었는지를 순서대로 보여주기 때문에 문제와 원인을 추적할 수 있는 중요한 단서를 제공합니다.
4️⃣ 호출 경로 이해.
프로그램에서 함수 호출 경로를 이해하는 데 도움을 주어, 코드의 흐름을 파악하고 수정할 수 있게 합니다.
2️⃣ 스택 트레이스의 구조.
스택 트레이스(Stack Trace)는 일반적으로 예외의 종류와 메시지, 그리고 함수 호출 스택의 목록으로 구성됩니다.
각 항목은 다음 정보를 포함합니다.
예외의 종류와 메시지
호출된 메서드의 목록.
1️⃣ 예외의 종류와 메시지.
예외의 유형과 메시지를 통해 예외의 원인에 대한 기본적인 정보를 제공합니다.
2️⃣ 호출된 메서드의 목록.
예외가 발생한 시점까지의 함수 호출 경로를 위에서부터 아래로 표시합니다.
가장 위의 항목은 예외가 실제로 발생한 메서드이며, 그 아래는 예외가 발생하기까지 호출된 메서드들이 표시됩니다.
3️⃣ 스택 트레이스 예시 (Java)
아래의 코드는 Java 프로그램에서 NullPointerException이 발생했을 때의 스택 트레이스 예시입니다.
public class StackTraceExample {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
String str = null;
str.length(); // 여기에서 NullPointerException 발생
}
}
👉 출력되는 스택 트레이스.
Excption in thread "main" java.lang.NullPointerException
at StackTraceExample.secondMethod(StackTraceExample.java:14)
at StackTraceExample.firstMethod(StackTraceExample.javee:9)
at StackTraceExample.main(StackTraceExample.java:5)
4️⃣ 스택 트레이스의 구성 요소.
1️⃣ 예외의 종류와 메시지.
Exception in thread "main" java.lang.NullPointerException
프로그램의 메인 스레드에서 NullPointerException이 발생했다는 것을 알려줍니다.
2️⃣ 호출된 메서드의 목록.
at StackTraceExample.secondMethod(StackTraceExample.java:14)
secondMethod 메서드의 14번째 줄에서 예외가 발생했습니다.
at StackTraceExample.firstMethod(StackTraceExample.java:9)
firstMethod가 secondMethod를 호출했습니다.
at StackTraceExample.main(StackTraceExample.java:5)
main 메서드가 firstMethod를 호출했습니다.
스택 트레이스(Stack Trace)는 가장 최근에 호출된 메서드(예외가 발생한 메서드)부터 시작하여, 예외가 발생하기 전에 호출된 메서드들을 순차적으로 표시합니다.
5️⃣ 스택 트레이스의 중요성.
1️⃣ 오류의 원인 파악.
스택 트레이스를 보면, 예외가 발생한 정확한 위치와 어떤 함수에서 호출된 것인지를 알 수 있습니다.
이를 통해 오류의 원인을 신속하게 파악할 수 있습니다.
예를 들어, 위의 스택 트레이스(Stack Trace)에서는 secondMethod의 str.length() 호출에서 NullPointerException이 발생했음을 알 수 있습니다.
그 이후에는 firstMethod가 이 메서드를 호출했고, 최종적으로 main 메서드에서 firstMethod가 호출되었습니다.
2️⃣ 디버깅 시간 단축.
스택 트레이스(Stack Trace)는 예외의 원인을 정확하게 지목하기 때문에, 개발자가 코드를 수정하고 디버깅하는 시간을 단축할 수 있습니다.
디버거를 사용하지 않고도 문제의 근본 원인을 빠르게 찾을 수 있습니다.
3️⃣ 호출 경로 이해.
코드의 흐름을 이해하는 데 두움을 줍니다.
특히, 복잡한 프로그램이나 타사의 코드를 다룰 때 스택 트레이스(Stack Trace)는 코드가 어떻게 작동하는지에 대한 중요한 단서를 제공합니다.
6️⃣ 스택 트레이스 읽는 방법.
1️⃣ 예외 메시지를 확인.
예외 메시지는 문제의 종류와 원인에 대해 첫 번째 힌트를 제공합니다.
예를 들어, NullPointException은 널 객체에 접근하려고 했다는 것을 의미합니다.
2️⃣ 가장 위의 호출 위치 찾기.
스택 트레이스(Stack Trace)에서 가장 위의 호출 위치는 예외가 발생한 정확한 위치를 나타냅니다.
이 부분부터 문제를 추적하기 시작해야 합니다.
3️⃣ 호출 순서대로 확인.
예외가 발생한 코드의 흐름을 이해하려면 호출 순서대로 각 메서드가 어떤 역할을 하는지 분석합니다.
이 과정을 통해 문제가 있는 부분을 빠르게 찾을 수 있습니다.
7️⃣ 스텍 트레이스를 유용하게 사용하는 방법.
1️⃣ 로깅(logging)과 함꼐 사용.
실제로 배포된 소프트웨어에서는 오류가 발생하면 로그에 스택 트레이스(Stack Trace)를 기록하도록 설정하는 것이 중요합니다.
이를 통해 사용자가 오류를 보고했을 때, 개발자가 쉽게 문제의 원인을 파악할 수 있습니다.
Java에서는 예외를 잡을 때 e.printStackTrace()를 사용하여 스택 트레이스(Stack Trace)를 출력하거나, 로깅 프레임워크(logging framework)를 사용하여 로그 파일에 기록할 수 있습니다.
2️⃣ 예외 처리에서 스택 트레이스 활용.
예외를 잡아서 처리할 때, 스택 트레이스를 출력함으로써 디버깅에 도움이 될 수 있습니다.
특히, 예상하지 못한 예외를 디버깅할 때 유용합니다.
8️⃣ 요약.
스택 트레이스(Stack Trace)는 프로그램에서 예외가 발생했을 때 예외의 원인과 호출 경로를 보여주는 디버깅 정보입니다.
예외의 종류와 메시지, 그리고 함수 호출 스택 목록으로 구성되어 있으며, 문제를 추적하고 해결하는 데 큰 도움이 됩니다.
이를 통해 개발자는 코드의 흐름을 이해하고, 예외가 발생한 정확한 위치와 원인을 파악하여 디버깅 시간을 단축할 수 있습니다.
스택 트레이스(Stack Trace)는 특히 로깅(logging)과 결합하여 사용하면, 배포된 소프트웨어의 문제를 진단하는 데 유용합니다.
-
💾 [CS] 객체의 생성과 소멸 과정은 무엇일까요?
💾 [CS] 객체의 생성과 소멸 과정은 무엇일까요?
객체의 생성과 소멸 과정은 프로그램이 객체를 메모리에 할당하고 사용하는 과정과, 더 이상 필요하지 않을 때 메모리에서 해제하는 과정을 말합니다.
객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서 이 과정은 매우 중요한 개념으로, 객체가 어떻게 생성되고, 소멸되는지 이해하면 더 효율적이고 메모리 누수(Memory leak)가 없는 프로그램을 작성할 수 있습니다.
🙋♂️ 객체 지향 프로그래밍(Object-Oriented Programming, OOP)는 무엇일까요?
1️⃣ 객체의 생성 과정.
객체가 생성될 때는 다음과 같은 과정을 거칩니다.
메모리 할당.
생성자 호출.
객체의 초기화.
메모리 주소 반환.
1️⃣ 메모리 할당.
객체를 생성하기 위해 먼저 메모리 할당이 이루어집니다.
자바의 경우, 객체는 힙 메모리(Heap)에 할당됩니다.
객체를 생성할 때는 new 키워드를 사용하여 힙 메모리(Heap)에서 필요한 만큼의 메모리를 할당합니다.
MyClass obj = new MyClass();
위 코드에서 new MyClass()는 힙 메모리(Heap)에 MyClass 타입의 객체를 생성하고, 생성된 객체의 매모리 주소를 obj 변수에 할당합니다.
📝 힙 메모리(Heap Memory)
힙 메모리(Heap Memory)는 프로그램 실행 중에 동적으로 메모리를 할당할 수 있는 영역으로, 필요한 만큼의 메모리를 런타임에 동적으로 할당하고 해제할 수 있습니다.
이는 전역 변수나 지역 변수처럼 컴파일 시점에 크가가 결정되지 않고, 프로그램 실행 중에 메모리 요구량이 달라질 때 유용하게 사용됩니다.
2️⃣ 생성자(Constructor) 호출.
메모리가 할당되면, 생성자(Constructor)가 호출되어 객체의 초기화 작업이 이루어집니다.
생성자(Constructor)는 객체의 초기 상태를 설정하고 필요한 리소스를 준비하는 역할을 합니다.
개발자는 객체를 생성할 때 생성자를 통해 객체의 초기값을 설정할 수 있습니다.
public class MyClass {
private int value;
// 생성자
public MyClass(int value) {
this.value = value; // 초기화 작업
}
}
MyClass obj = new MyClass(10); // 생성자를 통해 초기값 설정.
3️⃣ 객체의 초기화.
생성자가 호출되면서 객체의 필드(멤버 변수)가 초기화됩니다.
이 단계에서 객체의 상태가 설정되고, 필요한 리소스가 준비됩니다.
자바는 기본 생성자(인자가 없는 생성자)를 제공하여, 개발자가 별도로 생성자를 정의하지 않아도 객체를 생성할 수 있습니다.
4️⃣ 메모리 주소 반환.
객체가 생성된 후, 참조 변수는 생성된 객체의 메모리 주소를 가리키게 됩니다.
이로 인해 생성된 객체를 프로그램 내에서 접근하고 사용할 수 있게 됩니다.
2️⃣ 객체의 소멸 과정.
객체의 소멸은 더 이상 사용되지 않는 객체를 메모리에서 해제하는 과정입니다.
객체의 소멸은 언어에 따라 다르게 처리됩니다.
1️⃣ 자바에서의 가비지 컬렉션(Garbage Collection)
자바는 객체의 소멸을 개발자가 직접 제어하지 않고, 가비지 컬렉터(Garbege Collector)가 자동으로 처리합니다.
가비지 컬렉터(Garbage Collector)는 더 이상 참조되지 않는 객체를 감지하고, 힙 메모리(Heap Memory)에서 해제하여 메모리 누수(Memory Leak)를 방지합니다.
가비지 컬렉터(Garbage Collector)는 백그라운드에서 주기적으로 실행되며, 사용되지 않는 객체를 탐지하여 메모리에서 제거합니다.
📝 메모리 누수(Memory Leak)
프로그램이 더 이상 사용하지 않는 메모리를 해제하지 않고 계속해서 점유하고 있는 상태를 말합니다.
메모리 누수(Memory Leak)가 발생하면 사용하지 않는 메모리가 계속 쌓여서 시스템의 가용 메모리가 줄어들게 되고, 결국에는 프로그램이나 시스템 전체가 메모리 부족으로 인해 성능 저하를 일으키거나 비정상 종료될 수 있습니다.
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
obj1 = null; // obj1이 참조하던 객체는 더 이상 사용되지 않음
위 코드에서 obj1이 가리키던 객체는 더 이상 참조되지 않으므로, 가비지 컬렉터(Garbage Collector)가 이 객체를 메모리에서 제거할 수 있습니다.
2️⃣ 객체의 소멸과 종료자(Finalizer)
자바는 객체가 소멸되기 전에 호출되는 finalize() 메서드를 제공합니다.
그러나 가비지 컬렉터(Garbage Collector)의 동작을 제어하거나 보장할 수 없기 때문에, finalize()는 권장되지 않습니다.
가비지 컬렉션이 언제 일어날지 예측할 수 없으므로, 리소스를 명시적으로 해제할 필요가 있다면, try-with-resource나 finally 블록을 사용하는 것이 좋습니다.
3️⃣ 가비지 컬렉터가 객체를 소멸시키는 기준.
객체가 더 이상 참조되지 않거나, 순환 참조가 발생해 사용되지 않는 경우 가비지 컬렉터(Garbage Collector)는 이 객체를 메모리에서 제거할 수 있습니다.
📝 순환 참조(Circular Reference)
두 개 이상의 객체나 리소스가 서로를 참조하면서 참조의 순환 고리가 형성되는 상황을 의미합니다.
이러한 상황이 발생하면, 객체나 리소스가 서로를 참조하고 있어 해제되지 않는 상태가 되며, 특히 메모리 누수를 유발할 수 있습니다.
순환 참조(Circular Reference)는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서 객체 간의 관계가 복잡하게 얽힐 때 발생할 수 있으며, 주로 동적 메모리 할당과 수동 메모리 해제가 필요한 언어(C/C++)에서 문제가 됩니다.
Java, Python과 같은 가비지 컬렉션이 있는 언어에서도 문제가 될 수 있지만, 순환 참조(Circular Reference)를 처리할 수 있는 메커니즘(예: 약한 참조)이 존재합니다.
가비지 컬렉터(Garbage Collector)는 메모리 압박이 심하거나 프로그램이 오랜 시간 동안 실행될 때 주기적으로 실행됩니다.
3️⃣ 다른 언어에서의 객체 소멸.
1️⃣ C++와 수동 메모리 관리.
C++에서는 자바와 달리 수동 메모리 관리가 필요합니다.
개발자가 객체의 소멸 시점에 delete를 사용하여 직접 메모리를 해제해야 합니다.
C++에서는 소멸자(Destructor)가 있어 객체가 메모리에서 해제될 때 자동으로 호출되어, 메모리 해제나 리소스 반환 작업을 수행할 수 있습니다.
class MyClass {
public:
~MyClass() { // 소멸자
// 메모리 해제 작업 수행
}
};
MyClass* obj = new MyClass();
delete obj; // 직접 메모리 해제
2️⃣ 파이썬과 참조 카운팅
파이썬은 참조 카운팅(Reference Counting)을 사용하여 객체의 소멸을 관리합니다.
객체를 참조하는 변수가 없을 때, 해당 객체는 메모리에서 해제됩니다.
그러나 순환 참조(Circular Reference) 문제가 발생할 수 있어, 파이썬은 추가로 가비지 컬렉션도 사용합니다.
4️⃣ 객체 생성과 소멸에서 주의할 점.
1️⃣ 메모리 누수(Memory Leak).
객체를 더 이상 사용하지 않음에도 불구하고, 참조를 해제하지 않으면 메모리가 반환되지 않고 계속 사용된 상태로 남을 수 있습니다.
이를 메모리 누수(Memory Leak)라고 하며, 프로그램 성능에 치명적인 영향을 미칠 수 있습니다.
자바에서는 가비지 컬렉터가 자동으로 메모리를 해제하지만, C++와 같은 언어에서는 직접 메모리 관리를 잘해야 합니다.
2️⃣ 정확한 리소스 해제.
데이터베이스 연결, 파일 입출력, 네트워크 소켓 등과 같은 외부 리소스는 명시적으로 해제해야 합니다.
자바에서는 try-with-resources를 사용하여 이러한 리소스를 자동으로 해제할 수 있습니다.
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// 파일을 읽는 작업 수행
} // try 블록이 끝나면 BufferedReader가 자동으로 닫힘
📝 네트워크 소켓(Network Socket)
네트워크를 통해 통신을 수행하기 위한 양 끝단의 연결 인터페이스로, 두 컴퓨터 간에 데이터 통신을 할 수 있게 해주는 소프트웨어 엔드포인트입니다.
소켓(Socket)은 네트워크 연결을 설정하고 데이터를 주고받기 위한 네트워크 프로그래밍의 기본 단위로 사용됩니다.
📝 소프트웨어 인터페이스(Software Interface)
소프트웨어 시스템이나 애플리케이션이 다른 소프트웨어, 하드웨어, 사용자 또는 시스템과 상호작용하는 방식을 정의하는 개념입니다.
인터페이스(Interface)는 소프트웨어 컴포넌트(Component) 간의 커뮤니케이션 방법을 표준화하여, 서로 다른 시스템이나 모듈이 효율적으로 데이터를 주고받고 기능을 호출할 수 있도록 해줍니다.
5️⃣ 요약.
객체의 생성은 메모리 할당, 생성자 호출, 초기화 작업을 포함하며, 힙 메모리(Heap Memory)에서 이루어집니다.
반대로 객체의 소멸은 더 이상 사용되지 않는 객체를 메모리에서 해제하는 과정으로, 자바에서는 가비지 컬렉터(Garbage Collector)가 이를 자동으로 처리합니다.
객체의 생성과 소멸을 잘 이해하고 관리하면, 메모리 누수 없이 효율적인 프로그램을 작성할 수 있습니다.
개발자는 객체의 생명 주기를 잘 파악하고, 적절한 시점에 리소스를 해제하여 프로그램의 성능을 최적화해야 합니다.
-
💾 [CS] 스택 오버플로우(Stack Overflow)란 무엇일까요?
💾 [CS] 스택 오버플로우(Stack Overflow)란 무엇일까요?
스택 오버플로우(Stack Overflow)는 프로그램이 사용 가능한 스택 메모리(Stack Memory) 영역을 초과하여 더 이상 데이터를 쌓을 수 없게 되는 현상을 말합니다.
이로 인해 프로그램이 비정상적으로 종료되거나 시스템 에러가 발생하게 됩니다.
1️⃣ 스택(Stack)이란?
스택(Stack)은 프로그램 실행 시 함수 호출, 로컬 변수 등을 저장하는 메모리 영역입니다.
스택(Stack)은 후입선축(LIFO: Last In, First Out) 방식으로 작동하며, 함수가 호출될 때마다 해당 함수의 정보(예: 매개변수, 로컬 변수, 반환 주소 등)가 스택(Stack)에 “쌓이고(push)”, 함수가 졸요되면 스택(Stack)에서 “제거(pop)”됩니다.
2️⃣ 스택 오버플로우(Stack Overflow)의 원인.
1️⃣ 무한 재귀 호출(Recursive Call)
스택 오버플로우(Stack Overflow)가 가장 흔히 발생하는 경우는 재귀 함수가 종료되지 않고 무한히 호출될 때입니다.
재귀 함수는 자기 자신을 호출하면서 매번 새로운 스택 프레임(Stack frame)을 쌓게 되는데, 종료 조건 없이 계속 호출되면 스택에 계속 쌓이게 되어 스택 메모리(Stack Memory)를 초과하게 됩니다.
👉 예시
public class StackOverflowExample {
public static void recursiveMethod() {
// 재귀 호출 - 종료 조건이 없어 무한 호출됨
recursiveMethod();
}
public static void main(String[] args) {
recursiveMethod();
}
}
위 코드에서는 recursiveMethod()가 자기 자신을 계속해서 호출하기 때문에 스택 오버플로우(Stack Overflow)가 발생하게 됩니다.
2️⃣ 지나치게 깊은 함수 호출 체인.
여러 함수가 서로를 호출하는 상황에서도 스택 오버플로우(Stack Overflow)가 발생할 수 있습니다.
예를 들어, A() 함수가 B()를 호출하고, B()가 C()를 호출하고, … 이 과정을 매우 깊게 반복하면 스택 메모리(Stack Memory)가 초과될 수 있습니다.
📝 스택 메모리(Stack Memory)
프로그램 실행 중에 함수 호출과 관련된 임시 데이터를 저장하는 메모리 영역입니다.
주로 지역 변수, 함수 매개변수, 리턴 주소 등의 데이터를 저장하며, 함수가 호출될 때마다 새로운 스택 프레임(Stack Frame)이 생성되고, 함수가 종료되면 해당 스텍 프레임이 제거됩니다.
스택 메모리(Stack Memory)는 LIFO(Last In, First Out, 후입선출) 구조로 작동합니다.
즉, 마지막에 저장된 데이터가 먼저 제거되며, 함수 호출이 중첩될수록 스택(Stack)의 깊이가 깊어집니다
3️⃣ 너무 큰 로컬 변수 할당.
함수 내부에서 너무 큰 크기의 배열이나 객체를 로컬 변수로 선언할 때도 스택 오버플로우(Stack Overflow)가 발생할 수 있습니다.
스택(Stack)은 보통 크기가 제한된 메모리 영역이기 때문에, 너무 많은 메모리를 사용하는 로컬 변수를 선언하면 스택 메모리(Stack Memory)를 초과하게 됩니다.
3️⃣ 스택 오버플로우의 결과.
스택 오버플로우(Stack Overflow)가 발생하면 프로그램이 비정상적으로 종료되거나, JVM(Java Virtual Machine)에서 StackOverflowError와 같은 에러가 발생합니다.
재귀 함수 호출이나 과도한 함수 호출 체인을 사용할 때, 이런 문제가 발셍힐 수 있음을 인지하고 예방하는 것이 중요합니다.
4️⃣ 스택 오버플로우(StackOverflow)를 방지하는 방법.
1️⃣ 재귀 함수의 종료 조건 확인.
재귀 함수를 사용할 때는 반드시 종료 조건(Base case, 베이스 케이스)을 명확히 정의해야 합니다.
종료 조건(Base case, 베이스 케이스)이 없다면 무한히 호출되기 때문에 스택 오버플로우(StackOverflow)를 유발할 수 있습니다.
👉 예시
public class FactorialExample {
public static int factorial(int n) {
if (n <= 1) {
return 1; // 종료 조건: n이 1 이하일 때 재귀 호출 종료
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 정상적으로 120 출력
}
}
2️⃣ 재귀 대신 반복문 사용.
가능한 경우 재귀 함수 대신 반복문(loop)을 사용하여 스택 메모리(Stack Memory) 사용을 줄일 수 있습니다.
반복문(loop)은 스택 메모리(Stack Memory) 대신 힙 메모리(Heap Memory)나 CPU 레지스터(CPU Register)를 사용하므로, 깊은 재귀 호출을 반복문으로 변경하면 스택 오버플로우(StackOverflow)를 방지할 수 있습니다.
3️⃣ 꼬리 재귀 최적화(Tail Recursion Optimization)
일부 언어는 꼬리 재귀(tail recursion)를 최적화하여 재귀 호출을 반복문처럼 처리할 수 있습니다.
하지만 Java는 기본적으로 꼬리 재귀(tail recursion) 최적화를 지원하지 않기 때문에, 주의가 필요합니다.
꼬리 재귀(Tail Recursion)는 재귀 호출이 함수의 마지막 작업으로 실행될 때, 컴파일러가 스택 메모리(Stack Memory)를 재사용하여 스택 오버플로우(Stack Overflow)를 방지하는 방식입니다.
📝 꼬리 재귀(Tail Recursion)
재귀 함수의 한 형태로, 함수의 마지막(꼬리) 동작이 자기 자신을 호출하는 것을 말합니다.
즉, 재귀 호출 뒤에 추가적인 연산을 수행하지 않는 재귀 방식입니다.
꼬리 재귀(Tail Recursion)는 재귀 호출이 끝날 때 더 이상 처리할 작업이 남아 있지 않은 경우에 해당합니다.
4️⃣ 적절한 스택 크기 설정.
Java에서는 JVM(Java Virtual Machine)의 스택 크기를 설정하여 스택 오버 플로우(Stack Overflow)를 예방할 수 있습니다.
-Xss 옵션을 사용하여 스택 크기를 조정할 수 있습니다.
그러나, 스택 크기를 무작정 늘리는 것은 근본적인 해결책이 아니기 때문에 재귀 호출이나 함수 설계를 개선하는 것이 더 좋습니다.
👉 예시
java -Xss2m MyProgram
위 명령은 Java 프로그램의 스택 크기를 2MB로 설정합니다.
5️⃣ 요약
스택 오버 플로우(Stack Overflow)는 함수 호출이 스택 메모리(Stack Memory)의 한계를 초과했을 때 발생하며, 주로 재귀 함수의 무한 호출이나 깊은 함수 호출 체인에서 발생합니다.
이를 방지하기 위해 재귀 함수의 종료 조건을 명확히 설정하고, 필요한 경우 반복문을 사용하거나 JVM(Java Virtual Machine)의 스택 크기를 조정할 수 있습니다.
스택 오버플로우(Stack Overflow)를 피하기 위해서는 프로그램의 메모리 사용과 함수 호출 구조에 대한 철저한 이해와 관리가 필요합니다.
-
💾 [CS] 알고리즘의 공간 복잡도(Space Complexity)란 무엇일까요?
💾 [CS] 알고리즘의 공간 복잡도(Space Complexity)란 무엇일까요?
공간 복잡도(Space Complexity)는 알고리즘이 실행되는 동안 사용하는 메모리 공간의 양을 측정하는 지표입니다.
시간 복잡도(Time Complexity)가 알고리즘의 실행 시간을 평가하는 것이라면, 공간 복잡도(Time Complexity)는 알고리즘이 얼마나 많은 메모리를 사용하는지를 평가하는 개념입니다.
1️⃣ 왜 공간 복잡도가 중요한가요?
1️⃣ 메모리 효율성.
메모리는 제한된 자원입니다.
특히 임베디드 시스템이나 모바일 애플리케이션처럼 메모리 자원이 제한된 환경에서 공간 복잡도(Space Complexity)를 고려하지 않으면 메모리 부족으로 프로그램이 충돌하거나 성능이 저하될 수 있습니다.
📝 임베디드 시스템(Embedded System)
특정 기능을 수행하기 위해 설계된 독립적인 컴퓨터 시스템으로, 일반적인 컴퓨터와 달리 특정 목적이나 작업에 최적화가 되어 있습니다.
임베디드 시스템(Embadded System)은 하드웨어와 소프트웨어가 조합되어 특정 장치나 시스템의 일부로 내장되며, 자동차, 가전제품, 의료기기, 산업 장비, 가전 제품 등 다양한 곳에서 사용됩니다.
2️⃣ 성능 최적화.
때로는 알고리즘의 실행 속도뿐만 아니라 메모리 사용량도 최적화해야 합니다.
공간 복잡도(Space Complexity)를 줄이면 프로그램의 전반적인 성능이 향상될 수 있습니다.
3️⃣ 알고리즘 비교.
두 알고리즘이 같은 시간 복잡도(Time Complexity)를 가지더라도, 공간 복잡도(Space Complexity)가 낮은 알고리즘(Algorithm)이 더 효율적일 수 있습니다.
2️⃣ 공간 복잡도의 정의.
공간 복잡도(Time Complexity)는 알고리즘이 실행되면서 사용하는 총 메모리 양을 의미합니다.
이 메모리는 다음 두 가지로 나뉩니다.
고정 공간(Fixed Part)
가변 공간(Variable Part)
1️⃣ 고정 공간(Fixed Part)
알고리즘이 고정된 크기로 사용하는 메모리입니다.
입력 크기와 무관하게 항상 일정한 공간을 차지합니다.
예를 들어, 알고리즘에서 사용하는 상수, 변수, 함수 호출, 그리고 특정 크기의 고정 배열 등이 포함됩니다.
일반적으로 O(1)로 표현됩니다.
2️⃣ 가변 공간(Variable Part)
입력 크기에 따라 변화하는 메모리입니다.
예를 들어, 동적 배열, 재귀 호출 시 사용되는 스택 공간, 데이터를 저장하기 위한 임시 배열 등이 여기에 해당합니다.
가변 공간(Variable Part) 크기는 입력 크기에 따라 달라지며, 공간 복잡도(Space Complexity)의 중요한 요소가 됩니다.
3️⃣ 빅-오 표기법(Big-O Notation)으로 공간 복잡도 표현.
공간 복잡도(Space Complexity)도 시간 복잡도(Time Complexity)처럼 빅-오 표기법(Big-O Notation)을 사용하여 표현합니다.
이는 입력 크기(n)가 커질 때 메모리 사용량이 얼마나 빠르게 증가하는지를 나타냅니다.
1️⃣ 주요 공간 복잡도 예시.
1️⃣ O(1) 👉 상수 공간(Constant Space)
알고리즘이 입력 크기와 관계없이 일정한 양의 메모리를 사용하는 경우.
예: 변수 3개만을 사용하는 알고리즘.
int add(int a, int b) {
int sum = a + b;
return sum;
}
위 함수는 a, b, sum이라는 3개의 변수만을 사용하므로 O(1)의 공간 복잡도(Space Complexity)를 가집니다.
2️⃣ O(n) 👉 선형 공간(Linear Space)
입력 크기 n에 비례하여 메모리 공간이 증가하는 경우.
예: 크기가 n인 배열을 사용하는 알고리즘.
int[] copyArray(int[] arr) {
int[] newArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
newArr[i] = arr[i];
}
return newArr;
}
입력 배열의 크기 n에 비례하는 크기의 새로운 배열 newArr를 생성하므로 O(n)의 공간 복잡도(Space Complexity)를 가집니다.
3️⃣ O(n^2) 👉 이차 공간(Quadratic Space)
메모리 사용량이 입력 크기의 제곱에 비례하여 증가하는 경우.
예: 크기 n * n인 2차원 배열을 사용하는 알고리즘.
int[][] generateMatrix(int n) {
int [][] matrix = new int[n][n];
// 메모리 사용량이 n^2에 비례
return martix;
}
4️⃣ 예시를 통한 공간 복잡도 분석.
1️⃣ O(1) 공간 복잡도.
int findMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
분석
이 함수는 입력 배열의 크기와 상관없이 max와 i 두 개의 변수만 사용합니다.
따라서 공간 복잡도(Space Complexity)는 O(1)입니다.
2️⃣ 재귀 알고리즘의 공간 복잡도.
int factorial(int n) {
if(n <= 1) {
return 1;
}
return n * fatorial(n - 1);
}
분석
이 재귀 함수는 스택(Stack)을 사용하여 각 함수 호출을 저장합니다.
factorial(5)를 호출하면 내부적으로 factorial(4), factorial(3) 등이 호출되며, 호출이 끝나기 전까지 각 호출이 스택(Stack)에 저장됩니다.
재귀 깊이는 최대 n이 되므로, 공간 복잡도는 O(n)입니다.
5️⃣ 공간 복잡도 최적화 방법.
1️⃣ 데이터 구조 선택.
더 적은 메모리를 사용하는 효율적인 데이터 구조를 사용합니다.
예를 들어, 고정 크기의 배열 대신 동적 배역(ArrayList)을 사용하여 메모리를 절약할 수 있습니다.
2️⃣ 인플레이스(In-Place) 알고리즘.
입력 데이터를 그 자리에서 직접 수정하는 방식으로 메모리 사용을 최소화합니다.
예를 들어, 배열을 정렬할 때 새로운 배열을 생성하지 않고 기존 배열을 정렬하는 방식으로 공간 복잡도(Space Complexity)를 O(1)로 줄일 수 있습니다.
3️⃣ 재귀 대신 반복문 사용.
재귀 호출로 인한 스택 메모리(Stack Memory) 사용을 줄이기 위해 반복문을 사용할 수 있습니다.
재귀 함수가 깊은 재귀 호출을 통해 많은 메모리를 사용하는 경우, 이를 반복문으로 대체하면 공간 복잡도(Space Complexity)를 줄일 수 있습니다.
6️⃣ 요약.
공간 복잡도(Space Complexity)는 알고리즘(Algorithm)이 실행 중 사용하는 메모리의 양을 나타내며, 효율적인 알고리즘(Algorithm) 설계에 중요한 요소입니다.
이는 입력 크기에 따라 증가하는 메모리 사용량을 분석하며, 빅-오 표기법(Big-O Notation)을 사용하여 표현됩니다.
공간 복잡도(Space Complexity)를 잘 이해하면, 메모리 효율성을 높이고, 프로그램 성능을 최적화할 수 있습니다.
-
💾 [CS] 알고리즘의 시간 복잡도(Time Complexity)란 무엇일까요?
💾 [CS] 알고리즘의 시간 복잡도(Time Complexity)란 무엇일까요?
시간 복잡도(Time Complexity)는 알고리즘이 입력 크기에 따라 실행하는 연산의 수를 측정하는 지표로, 알고리즘의 효율성을 평가하는 데 사용됩니다.
시간 복잡도(Time Complexity)는 주로 입력 크기(n)가 커질수록 알고리즘의 실행시간이 얼마나 빨리 증가하는 지를 나타내며, 빅-오 표기법(Big-O Notaion)으로 표현됩니다.
1️⃣ 왜 시간 복잡도(Time Complexity)가 중요한가요?
1️⃣ 효율적인 알고리즘 설계.
알고리즘의 효율성을 평가할 때, 입력 데이터의 크기가 커질수록 얼마나 빠르게 실행 되는지를 고려해야 합니다.
시간 복잡도(Time Complexity)를 통해 알고리즘(Algorithm)의 성능을 비교하고, 더 나은 알고리즘(Algorithm)을 선택할 수 있습니다.
2️⃣ 대규모 데이터 처리.
실제 프로그램은 대규모 데이터를 처리해야 할 때가 많습니다.
시간 복잡도(Time Complexity)가 높은 알고리즘(Algorithm)은 큰 입력을 처리할 때 실행 시간이 크게 증가하여 성능이 저하될 수 있습니다.
3️⃣ 성능 예측.
시간 복잡도(Time Complexity)를 알면 입력 크기 변화에 따른 알고리즘의 성능을 예측할 수 있습니다.
이를 통해 최적화할 부분을 식별하고, 더 효율적인 코드로 개선할 수 있습니다.
2️⃣ 시간 복잡도(Time Complexity)의 기본 개념.
시간 복잡도(Time Complexity)는 입력 크기(n)에 따라 알고리즘이 수행해야 하는 기본 연산의 수를 나타냅니다.
일반적으로 알고리즘의 실행 시간을 절대적인 초 단위로 측정하지 않고, 연산 횟수로 추상화하여 표현합니다.
이는 하드웨어 성능에 따라 실제 시간은 다를 수 있지만, 연산 횟수는 동일하기 때문입니다.
시간 복잡도(Time Complexity)는 최악의 경우를 기준으로 하는 것이 일반적이며, 이는 알고리즘(Algorithm)의 성능을 보장하는 데 도움이 됩니다.
3️⃣ 빅-오 표기법(Big-O Notation)
빅-오 표기법(Big-O Notation)은 시간 복잡도(Time Complexity)를 표현하는 방식으로, 입력 크기(n)가 증가할 때 알고리즘의 실행 시간이 얼마나 빠르게 증가하는지를 나타냅니다.
1️⃣ 주요 빅-오 표기법.
1️⃣ O(1) 👉 상수 시간(Constant Time)
알고리즘의 실행 사간이 입력 크기와 상관업시 일정합니다.
예: 배열의 특정 인덱스에 접근하기(예: arr[0])
2️⃣ O(lon g) 👉 로그 시간(Longarithmic Time)
입력 크기가 커질 수록 실행 시간이 완만하게 증가합니다.
예: 이진 탐색(Binary Search)
3️⃣ O(n) 👉 선형 시간(Linear Time)
입력 크기에 비례하여 실행 시간이 증가합니다.
예: 배열에서 특정 값을 찾기 위해 모든 요소를 검사하는 경우.
4️⃣ O(n log n) 👉 선형 로그 시간(Linearithmic Time)
선형 시간보다 더 복잡한 시간 복잡도.
일반적으로 효율적인 정렬 알고리즘에서 발생합니다.
예: 퀵 정렬(Quick Sort), 병합 정렬(Merge Sort)
5️⃣ O(n^2) 👉 이차 시간(Quadratic Time)
입력 크기에 대해 제곱에 비례하여 실행 시간이 증가합니다.
일반적으로 중첩된 반복문에서 발생합니다.
예: 버블 정렬(Bubble Sort), 삽입 정렬(Insertion Sort)
6️⃣ O(2^n) 👉 지수 시간(Exponential Time)
입력 크기가 커질수록 실행 시간이 지수적으로 증가합니다.
보통 입력 크기가 작을 때만 사용할 수 있습니다.
예: 피보나치 수열을 재귀적으로 계산하는 알고리즘
7️⃣ O(n!) 👉 팩토리얼 시간(Factorial Time)
입력 크기가 커질수록 실행 시간이 매우 빠르게 증가합니다.
주로 모든 가능한 순열을 계산하는 알고리즘에서 발생합니다.
예: 순열(permutation) 생성.
4️⃣ 시간 복잡도의 예시.
1️⃣ O(1) 👉 상수 시간.
int getFirstElement(int[] arr) {
return arr[0]; // 입력 크기와 상관없이 일정한 시간
}
2️⃣ O(n) 👉 선형 시간.
int sum(int[] arr) {
int sum = 0;
for (int num : arr) {
sum += num; // 배열의 모든 요소를 순회
}
return sum;
}
3️⃣ O(n^2) 👉 이차 시간.
void printPairs(int[] arr) {
for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < arr.length; j++) {
System.out.println(arr[i] + ", " + arr[j]); // 중첩된 반복문
}
}
}
5️⃣ 시간 복잡도 분석 방법.
1️⃣ 반복문.
반복문이 한 번 실행될 때마다 O(n).
중첩된 반복문 O(n^2), O(n^3)와 같은 더 높은 차수의 시간 복잡도를 가집니다.
2️⃣ 조건문.
if-else 조건문은 입력 크기에 영향을 미치지 않는 경우 O(1)
3️⃣ 재귀 호출.
재귀 함수의 시간 복잡도는 재귀 호출의 깊이와 각 호출의 작업에 따라 달라집니다.
피보나치 재귀 함수는 O(2^n) 시간 복잡도를 가집니다.
4️⃣ 연산.
일반적인 산술 연산, 비교, 할당 등은 O(1)
6️⃣ 시간 복잡도 예측의 중요성.
시간 복잡도를 잘 이해하면, 알고리즘을 설계할 때 효율성을 예측할 수 있습니다.
코딩테스트나 실제 개발에서도 성능 최적화가 중요한데, 시간 복잡도(Time Complexity)를 통해 입력 크기에 따라 알고리즘이 어떻게 동작할지 미리 예상할 수 있습니다.
7️⃣ 요약.
시간 복잡도(Time Complexity)는 알고리즘이 입력 크기에 따라 얼마나 빠르게 실행 시간이 증가하는지를 평가하는 지표로, 알고리즘의 효율성을 비교하고 성능을 최적화하는 데 중요한 역할을 합니다.
빅-오 표기법(Big-O Notation)은 시간 복잡도(Time Complexity)를 표현하는 방식으로, 알고리즘의 성능을 추상적이고 간결하게 나타내오, 입력 크기 변화에 따른 알고리즘의 동작을 예측할 수 있게 도와줍니다.
효율적인 알고리즘을 설계하기 위해서는 시간 복잡도에 대한 깊은 이해가 필수적입니다.
-
💾 [CS] 프로그램 실행 원리를 설명하기 위해서는 컴퓨터 과학을 이해해야 할까요?
💾 [CS] 프로그램 실행 원리를 설명하기 위해서는 컴퓨터 과학(Computer Science)을 이해해야 할까요?
프로그램의 실행 원리를 자세히 이해하려면 컴퓨터 과학(Computer Science)의 여러 개념들을 이해하는 것이 매우 중요합니다.
컴퓨터 과학(Computer Science)은 프로그램이 어떻게 작성되고, 컴파일되며, 실행되는지를 설명하는 다양한 원리와 기술을 다룹니다.
프로그램의 실행 원리를 이해하기 위해 필요한 컴퓨터 과학(Computer Science)의 개념들은 컴퓨터 아키텍쳐(Computer Architecture), 운영 체제(Operating System, OS), 데이터 구조(Data Structure), 알고리즘(Algorithm) 등이 있습니다.
1️⃣ 프로그램의 실행 원리.
프로그램의 실행 과정은 코드를 작성하는 단계에서부터 프로그램이 실제로 CPU에서 실행되는 단계까지를 포함합니다.
이 과정은 다음과 같은 주요 단계로 나눌 수 있습니다.
1️⃣ 코드 작성(Programming)
개발자가 프로그래밍 언어를 사용하여 소스 코드를 작성합니다.
소스 코드는 텍스트 파일로 저장되며, 사람이 읽고 이해할 수 있는 형태입니다.
2️⃣ 컴파일 또는 인터프리트(Compilation or Interpretation)
프로그램이 작성된 후, 컴파일러(Compiler) 또는 인터프리터(Interpreter)가 소스 코드(Source Code)를 머신 코드(Machine Code)로 변환합니다.
컴파일러(Compiler)는 소스 코드를 한 번에 전체적으로 번역하여 실행 파일을 생성하는 반면, 인터프리터(Interpreter)는 코드를 한 줄씩 읽고 번역하면서 즉시 실행합니다.
이 과정에서는 어휘 분석, 구문 분석, 코드 최적화 등의 과정이 포함됩니다.
📝 어휘 분석(Lexical Analysis)
컴파일 과정의 첫 번째 단계로, 소스 코드에서 문자(character)들의 연속을 의미 있는 단위인 토큰(Token)으로 분해하는 과정입니다.
이 과정에서 어휘 분석기(Lexer) 또는 스캐너(Scanner)가 사용되며, 소스 코드의 텍스트를 읽어들여 키워드, 식별자, 연산자, 리터럴 등의 토큰을 생성합니다.
📝 구문 분석(Syntax Analysis)
컴파일 과정의 두 번째 단계로, 어휘 분석(Lexical Analysis)에서 생성된 토큰(Token)들을 받아서 소스 코드가 문법적으로 올바른지 확인하고, 이를 구조적으로 표현하는 과정입니다.
구문 분석기는 파서(Parser)라고도 하며, 프로그램 소스 코드를 구문 트리(Syntax Tree) 또는 파싱 트리(Parsing Tree)라는 트리 구조로 변환합니다.
3️⃣ 프로그램 로드(Program Loading)
컴파일된 실행 파일이 운영체제(Operating System, OS)에 의해 메모리에 로드됩니다.
운영체제(Operating System, OS)는 프로그램이 실행되기 위해 필요한 메모리 공간을 할당하고, 필요한 라이브러리(Library) 및 모듈(Module)을 로드합니다.
🙋♂️ 라이브러리(Library)와 프레임워크(Framework)의 차이점.
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
🙋♂️ 소프트웨어 공학에서의 모듈.
4️⃣ 프로그램 실행(Program Execution)
프로그램이 메모리에 로드된 후, CPU가 프로그램의 명령어를 하나씩 읽고 실행합니다.
CPU는 명령어 사이클(Fetch-Decode-Execute)을 반복하여 프로그램의 명령어를 처리합니다.
📝 명령어 사이클(Fetch-Decode-Execute Cycle)
명령어 사이클(Fetch-Decode-Execute Cycle)은 컴퓨터의 CPU가 프로그램을 실행하는 기본적인 작동 과정을 설명하는 개념입니다.
이는 CPU가 메모리에 저장된 명령어를 가져와(Fetch), 해석하고(Decode), 실행(Execute)하는 일련의 단계를 반복하여 프로그램을 처리하는 방법입니다.
모든 프로그램은 이 명령어 사이클(Fetch-Decode-Excute Cycle)을 통해 실행되며, 각 단계에서 CPU는 특정 작업을 수행하여 프로그램의 명령어를 처리합니다.
실행 중인 프로그램은 운영체제(Operating System, OS)에 의해 프로세스(Process)로 관리되며, 운영체제는 프로세스 스케줄링(Process Scheduling)을 통해 CPU 시간을 할당합니다.
📝 프로세스 스케줄링(Process Scheduling)
운영체제(Operating System, OS)가 프로세스(Process)를 CPU에 할당하여 실행 순서를 결정하는 작업을 말합니다.
컴퓨터 시스템에서 동시에 여러 프로세스가 실행을 대기하고 있을 때, 한정된 CPU 자원을 효율적으로 관리하고, 프로세스들을 효과적으로 실행할 수 있도록 스케줄링(Scheduling)하는 것이 필요합니다.
스케줄러(Scheduler)는 어떤 프로세스(Process)가 언제, 얼마나 오랫동안 CPU를 사용할지 결정하며, 이를 통해 멀티태스킹 환경에서 여러 프로세스가 동시에 실행되는 것처럼 보이도록 합니다.
📝 멀티태스킹(Multitasking)
컴퓨터가 동시에 여러 작업(프로세스 또는 프로그램)을 실행하는 기능을 의미합니다.
이는 사용자가 여러 프로그램을 동시에 실행하거나, 운영체제가 백그라운드에서 여러 작업을 병렬로 수행할 수 있도록 합니다.
멀티태스킹 덕분에 사용자들은 여러 개의 프로그램을 동시에 사용할 수 있는 환경을 경험하게 됩니다.
실제로는 CPU가 여러 작업을 빠르게 전환하면서 동시에 여러 작업을 수행하는 것처럼 보이게 하는 방식으로 구현됩니다.
각 작업은 아주 짧은 시간 동안 CPU를 사용하고, 이후 다른 작업으로 전환되는 과정을 반복합니다
이를 통해 여러 갖겅비 동시에 진행되는 것처럼 보이게 됩니다.
5️⃣ 메모리 및 자원 관리(Resource Management)
프로그램 실행 중에 필요한 메모리, 파일, 네트워크 연결 등 다양한 자원들이 운영체제(Operating System, OS)에 의해 관리됩니다.
운영체제(Operating System, OS)는 프로그램이 사용하는 자원을 추적하고, 충돌이나 충돌이 발생하지 않도록 합니다.
이 과정에서 가비지 컬렉션이나 메모리 해제등의 작업도 수행될 수 있습니다.
2️⃣ 컴퓨터 과학의 개념이 프로그램의 실행에 미치는 영향.
위의 단계들을 자세히 이해하기 위해서는 컴퓨터 과학(Computer Science)의 여러 분야에 대한 이해가 필요합니다.
1️⃣ 컴퓨터 아키텍쳐(Computer Architecture)
컴퓨터 아키텍처(Computer Architecture)는 CPU, 메모리, I/O 장치와 같은 컴퓨터 하드웨어의 구조와 작동 방식을 설명합니다.
프로그램이 실행될 때 CPU가 명령어를 어떻게 처리하는지, 메모리에 데이터가 어떻게 저장되고 접근되는지를 이해하려면 컴퓨터 아키텍쳐(Computer Architecture) 지식이 필요합니다.
예를 들어, 캐시 메모리, 파이프라인, 병렬 처리 같은 프로그램의 성능에 큰 영향을 미칠 수 있습니다.
📝 캐시 메모리(Cache Memory)
CPU와 메인 메모리(Random Access Memory, RAM) 사이에 위치한 고속의 작은 크기의 메모리로, 자주 사용되는 데이터나 명령어를 일시적으로 데이터나 명령어를 일시적으로 저장하여 CPU가 빠르게 접근할 수 있도록 하는 역할을 합니다.
캐시 메모리(Cache Memory)는 메인 메모리(Random Access Memory, RAM)보다 접근 속도가 훨씬 빠르기 때문에, 프로그램 실행 시 필요한 데이터와 명령어를 더 빨리 읽어 들일 수 있도록 해줍니다.
📝 파이프라인(Pipline)
파이프라인(Pipline)은 여러 작업을 연속적으로 처리하기 위해 각 작업을 여러 단계로 나누고, 동시에 처리할 수 있도록 설계한 기술을 의미합니다.
파이프라인(Pipline)은 컴퓨터의 CPU 설계에서 주로 사용되며, 명령어를 여러 단계로 나누어 각 단계가 병렬로 실행될 수 있게 함으로써 처리 속도를 높이는 방식입니다.
2️⃣ 운영체제(Operating System)
운영체제(Operating System)는 프로그램이 실행되기 위한 환경을 제공하고, 프로세스 관리, 메모리 관리, 파일 시스템 관리 등을 담당합니다.
프로그램의 실행 원리를 이해하려면 운영체제(Operating System, OS)가 어떻게 프로세스를 스케쥴링하고, 메모리를 관리하며, 입출력(I/O) 요청을 처리하는지에 대한 지식이 필요합니다.
예를 들어, 프로그램이 동시에 실행될 때 멀티태스킹과 스레드 관리가 어떻게 이루어지는지 이해해야 합니다.
3️⃣ 컴파일러 이론(Compiler Theory)
컴파일러(Compiler)는 소스 코드(Source Code)를 기계어로 변환하는 프로그램으로, 컴파일러 이론은 이 과정의 어휘 분석, 구문 분석, 최적화 등 다양한 간계를 설명합니다.
프로그램이 어떻게 최적화되어 더 빠르게 실행될 수 있는지, 어떤 코드가 더 효율적인지를 이해하려면 컴파일러(Compiler) 이론의 지식이 필요합니다.
📝 컴파일러(Compiler)
프로그래밍 언어로 작성된 소스코드를 기계어로 번역하여 실행 가능한 프로그램으로 만드는 소프트웨어입니다.
사람이 읽고 작성한 고수준 프로그래밍 언어(예: C, C++, Java, Python)를 컴퓨터가 이해할 수 있는 저수준 언어(기계어, 바이너리 코드)로 변환하는 역할을 합니다.
컴파일러는 프로그램을 실행하기 전에 한 번에 전체 소스 코드를 번역하고, 그 결과를 실행 파일(Executable File)로 생성합니다.
이 파일은 운영체제(Operating System, OS)에서 직접 실행될 수 있으며, 이후에는 별도의 과정 없이 바로 프로그램을 실행할 수 있습니다.
4️⃣ 데이터 구조(Data Structures)
프로그램은 데이터를 효율적으로 저장하고 관리하기 위해 다양한 데이터 구조를 사용합니다.
배열(Array), 연결 리스트(Linked List), 스택(Stack), 큐(Queue), 트리(Tree), 해시 테이블(Hash Table) 등과 같은 데이터 구조는 프로그램이 데이터를 처리하고 저장하는 방식에 큰 영향을 미칩니다.
프로그램의 실행 속도와 메모리 사용량을 최적화하려면 어떤 데이터 구조가 적합한지 이해하는 것이 중요합니다.
5️⃣ 알고리즘(Algorithms)
알고리즘은 문제를 해결하는 절차나 방법으로, 프로그램의 핵심 로직을 구성합니다.
정렬, 검색, 그래프 탐색, 동적 프로그래밍 등 다양한 알고리즘이 있으며, 효율적인 알고리즘을 설계하는 것은 프로그램의 성능에 직접적인 영향을 줍니다.
알고리즘의 시간 복잡도와 공간 복잡도를 이해하는 것은 프로그램이 어떤 속도로 실행되고 얼마나 많은 자원을 사용하는지를 파악하는 데 중요합니다.
6️⃣ 컴퓨터 네트워킹(Computer Networking)
네트워크를 통한 데이터 전송이 필요한 프로그램(예: 웹 애플리케이션, 클라우드 서비스 등)은 네트워크 프로토콜과 데이터 전송 방식을 이해해야 합니다.
프로그램이 데이터를 어떻게 전송하고 수신하는지, 네트워크 대기 시간이 프로그램의 성능에 어떻게 영향을 미치는지를 이해하려면 네트워크에 대한 지식이 필요합니다.
3️⃣ 프로그램 실행 원리와 컴퓨터 과학의 관계.
프로그램의 실행 원리는 컴퓨터 과학의 여러 하위 분야가 협력하여 작동하는 복합적인 과정입니다.
프로그램이 실행될 때, 컴퓨터 아키텍쳐, 운영체제, 컴파일러, 데이터 구조, 알고리즘이 서로 맞물려 작동하면서 프로그램이 효율적으로 실행되도록 합니다.
따라서 프로그램이 어떻게 메모리에 로드되고, CPU에서 처리되며, 자원이 관리되는지에 대해 깊이 이해하려면 컴퓨터 과학의 핵심 개념들을 공부해야 합니다.
4️⃣ 요약.
프로그램의 실행 원리를 깊이 이해하려면 컴퓨터 과학(Computer Science)의 다양한 개념을 알아야 합니다.
컴퓨터 과학은 컴퓨터 아키텍쳐, 운영체제, 컴파일러, 데이터 구조, 알고리즘, 네트워크 등을 포함하는 폭넓은 학문으로, 프로그램이 어떻게 작성되고, 컴파일되며, 실행되는지 설명하는 데 필요한 모든 이론적 기반을 제공합니다.
프로그램 실행의 각 단계는 컴퓨터 과학의 여러 개념이 상호작용하는 결과이며, 이러한 지식을 갖추면 프로그램을 최적화하고 효율적으로 설계하는 데 큰 도움이 됩니다.
-
🍃[Spring] SimpleJpaRepository란 무엇일까요?
🍃[Spring] SimpleJpaRepository란 무엇일까요?
SimpleJpaRepository는 Spring Data JPA(Java Persistence API)에서 제공하는 기본 Repository 구현 클래스입니다.
이 클래스는 Spring Data JPA의 Repository 인터페이스(CrudRepository, JpaRepository 등)를 구현하며, 일반적으로 사용되는 CRUD(생성(Create), 조회(Read), 수정(Update), 삭제(Delete)) 기능을 포함하고 있습니다.
1️⃣ 역할.
SimpleJpaRepository는 개발자가 정의한 Repository 인터페이스의 구현체로, 데이터베이스와의 상호작용을 간편하게 처리할 수 있도록 다양한 메서드를 제공합니다.
개발자는 직접 CRUD 로직을 구현하지 않고도, SimpleJpaRepository를 통해 자동으로 제공되는 기본 CRUD 메서드를 활용할 수 있습니다.
2️⃣ SimpleJpaRepository의 동작 방식.
1️⃣ 기본 CRUD 메서드 제공.
SimpleJpaRepository는 save, findById, findAll, delete 등과 같은 기본적인 CRUD(생성(Create), 조회(Read), 수정(Update), 삭제(Delete)) 메서드를 구현합니다.
개발자가 JpaRepository 인터페이스를 확장하여 커스텀 Repository 인터페이스를 정의하면, Spring Data JPA가 런타임 시점에 SimpleJpaRepository를 사용해 해당 인터페이스의 구현체를 자동으로 생성합니다.
2️⃣ 데이터베이스와의 자동 상호작용.
SimpleJpaRepository는 Hibernate와 같은 JPA(Java Persistence API) 구현체와 상호작용하여, 데이터를 데이터베이스에 저장하거나 조회할 때 필요한 SQL(Structured Query Language) 쿼리를 자동으로 생성하고 실행합니다.
개발자는 데이터베이스와의 직접적인 상호작용을 구현할 필요가 없므며, Spring Data JPA가 이 모든 것을 자동으로 처리합니다.
3️⃣ 쿼리 메서드 지원
SimpleJpaRepository는 메서드 이름을 분석하여 쿼리 메서드를 자동으로 생성하는 기능을 지원합니다.
예를 들어, findByName과 같은 메서드를 정의하면, Spring Data JPA는 자동으로 name 필드를 기준으로 데이터를 조회하는 쿼리를 생성합니다.
3️⃣ SimpleJpaRepository의 기본적인 CRUD 메서드.
SimpleJpaRepository는 다음과 같은 기본 메서드를 제공합니다.
save(S entity)
엔티티를 데이터베이스에 저장합니다. 엔티티가 이미 존재하면 업데이트하고, 존재하지 않으면 새로 삽입합니다.
findById(ID id)
지정된 ID로 데이터베이스에서 엔티티를 조회합니다.
findAll()
데이터베이스에 저장된 모든 엔티티를 조회합니다.
delete(T entity)
데이터베이스에서 엔티티를 삭제합니다.
deleteById(ID id)
지정된 ID로 데이터베이스에서 엔티티를 삭제합니다.
count()
데이터베이스에 저장된 엔티티의 총 개수를 반환합니다.
4️⃣ 사용 예시.
👉 UserRepository 인터페이스.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UseRepository extends JpaRepository<User, Long> {
User findByName(String name);
}
위 예시에서 UserRepository는 JpaRepository를 상속하고 있으며, 이를 통해 SimpleJpaRepository가 UserRepository의 구현체로 동작하게 됩니다.
findByName, save, findById 등의 메서드를 별도로 구현하지 않아도, Spring Data JPA가 SimpleJpaRepository를 사용하여 이들을 자동으로 제공해줍니다.
👉 서비스 클래스에서 사용.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Sevice
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String name, Integer age) {
User user = new User(name, age);
return userRepository.save(user); // SimpleJpaRepository의 save 메서드 사용
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id); // SimpleJpaRepository의 findById 메서드 사용
}
public void deleteUser(Long id) {
userRepository.deleteById(id); // SimpleJpaRepository의 deleteById 메서드 사용
}
}
5️⃣ SimpleJpaRepository의 확장 및 커스터마이징.
1️⃣ 기본 기능 확장.
개발자는 SimpleJpaRepository가 제공하는 기본 기능 외에 추가적인 커스텀 기능을 Repository에 구현할 수 있습니다.
예를 들어, 복잡한 쿼리를 직접 정의하거나, 추가적인 비즈니스 로직을 구현할 수 있습니다.
2️⃣ @Query 어노테이션 사용.
@Query 어노테이션을 사용하면 복잡한 JPQL(Java Persistence Query Language) 또는 네이티브 SQL 쿼리를 직접 작성하여 메서드에 연결할 수 있습니다.
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List<User> findByNameAndAgeGreaterThan(@Param("name") String name, @Param("age") Integer age);
}
6️⃣ 요약.
SimpleJpaRepository는 Spring Data JPA에서 기본적으로 제공하는 CRUD 기능의 구현체로, JpaRepository 인터페이스를 통해 자동으로 생성됩니다.
개발자는 JpaRepository를 상속받는 Repository 인터페이스를 정의함으로써 SimpleJpaRepository가 제공하는 자동 CRUD 기능을 활용할 수 있으며, 메서드 이름 기반 쿼리, 커스텀 쿼리 등을 통해 데이터베이스와의 상호작용을 간편하게 처리할 수 있습니다.
Spring Data JPA는 데이터베이스와의 상호작용을 단순화하고, 코드의 가독성과 유지보수성을 높이는 데 큰 역할을 합니다.
SimpleJpaRepository는 이러한 Spring Data JPA의 핵심을 담당하는 기본 구현체입니다.
-
☕️[Java] Java Stream이란 무엇일까요?
☕️[Java] Java Stream이란 무엇일까요?
Java Stream은 Java 8에서 도입된 데이터 처리 API로, 컬렉션(Collection), 배열(Array), 파일 등의 데이터 소스를 효율적으로 처리할 수 있게 해주는 기능입니다.
Stream을 사용하면 데이터의 필터링, 변환, 정렬, 집계 등의 작업을 함수형 스타일로 간결하게 작성할 수 있습니다.
1️⃣ Stream의 주요 특징.
1️⃣ 선언형 프로그래밍 스타일.
기존의 반복문을 사용한 명령형(Imperative) 코드와 달리, Stream은 선언형(Declarative) 스타일을 사용하여 더 간결하고 읽기 쉬운 코드를 작성할 수 있게 합니다.
2️⃣ 함수형 프로그래밍.
Stream은 함수형 프로그래밍을 지원하여, 람다 표현식(Lambda Expression)과 메서드 참조(Method Reference)를 사용하여 간결한 코드를 작성할 수 있습니다.
3️⃣ 데이터의 흐름.
Stream은 데이터 소스로부터 데이터 흐름을 처리하며, 데이터를 직접 저장하지 않습니다.
즉, 컬렉션이나 배열에 데이터를 저장하는 것이 아니라, 데이터를 처리하는 역속적인 작업을 제공합니다.
4️⃣ 지연 연산(Lazy Evaluation)
Stream은 지연 연산(Lazy Evaluation)을 사용하여 필요한 경우에만 데이터를 처리합니다.
중간 연산이 지연되어 최종 연산이 호출될 때만 실제로 데이터 처리가 이루어집니다.
5️⃣ 병렬 처리.
Stream API는 쉽게 병렬 처리를 수행할 수 있는 메서드를 제공하여, 멀티코어 시스템에서 성능을 최적화할 수 있습니다.
parallelStream() 메서드를 사용하면 병렬 처리를 활성화할 수 있습니다.
2️⃣ Stream의 기본 구성.
Stream은 크게 중간 연산과 최종 연산으로 구성됩니다.
👉 중간 연산.
필터링, 변환, 정렬 등과 같이 데이터의 흐름을 처리하는 작업을 수행하지만, 최종 연산이 호출되기 전까지는 실행되지 않습니다.(예: filter, map, sorted)
👉 최종 연산.
스트림의 데이터를 결과로 수집하거나 출력하는 연산으로, 최종 연산이 호출되면 스트림이 처리되고 종료됩니다.(예: collect, forEach, reduce)
3️⃣ Stream 사용 예시.
1️⃣ 컬렉션에서 Stream 사용.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Jhon", "Jane", "Jack", "Doe");
// 필터링과 변환
List<String> filteredNames = names.stream()
.filter(name -> name.startWith("J")) // "J"로 시작하는 이름만 필터링
.map(String::toUpperCase) // 대문자로 변환
.collect(Collectors.toList()); // 결과를 리스트로 수집
System.out.println(filteredNames) // 출력: [JHON, JANE, JACK]
}
}
4️⃣ Stream의 기본 연산.
👉 중간 연산.
1️⃣ filter(Predicate)
조건에 맞는 요소만 걸러냅니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4]
2️⃣ map(Function)
각 요소를 변환합니다.(예: 문자열을 대문자로 변환, 객체의 특정 필드를 추출)
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList()); // [5, 6, 7]
3️⃣ sorted()
요소를 정렬합니다. 기본적으로 오름차순 정렬하며, 커스텀 Comparator를 사용할 수도 있습니다.
List<Integer> sortedNumbers = numbers.stream()
.sorted
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
4️⃣ distinct()
중복된 요소를 제거합니다.
List<Integer> distinctNumbers = Arrays.asList(1, 2, 2, 3, 3, 3)
.stream()
.distinct()
.collect(Collectors.toList()); // [1, 2, 3]
👉 최종 연산.
1️⃣ collect(Collector)
스트림의 요소를 모아서 리스트, 집합 등의 컬렉션으로 변환합니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList()); // [JOHN, JANE, JACK]
2️⃣ forEach(Consumer)
각 요소에 대해 지정된 작업을 수행합니다.
출력이나 로그 작업 등에 사용됩니다.
names.stream()
.forEach(System.out::println);
3️⃣ reduce(BinaryOperator)
스트림의 요소를 누적하여 하나의 결과로 만듭니다.
주로 합계, 곱, 문자열 연결 등에 사용됩니다.
int sum = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce(0, Integer::sum) // 15
4️⃣ count()
스트림의 요소 개수를 반환합니다.
long count = names.stream()
.filter(name -> name.startWith("J"))
.count(); // 3
5️⃣ 병렬 스트림
Stream API는 병렬 처리를 쉽게 수행할 수 있도록 지원합니다.
paralleStream() 또는 parallel() 메서드를 사용하여 스트림을 병렬로 처리할 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum); // 병렬로 합 계산
병렬 스트림은 멀티 코어 시스템에서 성능을 최적화할 수 있지만, 병렬로 처리할 때는 데이터 간의 독립성이 보장되어야 합니다.
6️⃣ 요약.
Java Stream은 데이터의 필터링, 변환. 정렬, 집계 등을 간결하고 선언적인 스타일로 작성할 수 있는 기능을 제공합니다.
중간 연산과 최종 연산을 조합하여 복잡한 데이터 처리를 간단하게 구현할 수 있으며, 병렬 처리를 통해 성능을 향상시킬 수도 있습니다.
Stream API를 사용하면 기존의 반복문과 조건문을 대체할 수 있어, 코드의 가독성과 유지보수성이 크게 향상됩니다.
-
☕️[Java] Java Optional이란 무엇일까요?
☕️[Java] ☕️[Java] Java Optional이란 무엇일까요?
Optional은 Java 8에서 도입된 null 값을 안전한게 처리하기 위한 클래스입니다.
null 값을 직접 다루는 것에 대한 문제점(예: NullPointException)을 줄이기 위해 설계되었으며, 값이 존재할 수도 있고, 존재하지 않을 수도 있는 상황을 표현할 수 있도록 도와줍니다.
1️⃣ Optional의 주요 목적.
1️⃣ null 처리의 안정성
Java에서 null을 반환하거나 사용하는 것은 매우 흔한 일이지만, 잘못된 null 처리는 NullPointException을 유발할 수 있습니다.
Optional을 사용하면 이런 문제를 예방할 수 있습니다.
2️⃣ 의도 명확성.
Optional을 사용하면, 메서드의 반환값이 값이 없을 수 있음을 명확하게 표현할 수 있습니다.
개발자는 반환값이 Optional 타입일 때 값이 없는 경우에 대한 처리를 고려할 수 있습니다.
3️⃣ 코드의 가독성 및 유지보수성.
Optional은 null 체크를 일일이 하는 대신, 값이 없을 수 있는 상황을 안전하게 다루기 위한 여러 메서드를 제공합니다.
이를 통해 코드의 가독성이 좋아지고 유지보수하기 쉬워집니다.
2️⃣ Optional의 생성 방법.
1️⃣ Optional.of(T value)
null이 아닌 값으로 Optional 객체를 생성합니다.
값이 null일 경우 NullPointException이 발생합니다.
Optional<String> optional = Optional.of("Hello");
2️⃣ Optional.ofNullable(T value)
전달된 값이 null일 수도 있고, 아닐 수도 있는 경우 사용합니다.
값이 null이면 Optional.empty() 반환하고, 그렇지 않으면 Optional.of(value)와 동일한 동작을 합니다.
Optional<String> optional = Optional.ofNullable(null); // 비어있는 Optional 반환
3️⃣ Optional.empty()
비어있는 Optional 객체를 생성합니다.
값이 없음을 명시적으로 나타낼 때 사용합니다.
Optional<String> emptyOptional = Optional.empty();
3️⃣ Optional의 주요 메서드.
1️⃣ isPresent()
Optional에 값이 있으면 true, 없으면 false를 반환합니다.
Optional<String> optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println(optional.get()); // "Hello" 출력
}
2️⃣ ifPresent(Consumer<T> action)
Optional에 값이 있으면 해당 값으로 지정된 동작을 수행합니다.
값이 없으면 아무 일도 하지 않습니다.
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(value -> System.out.println(value)) // "Hello" 출력
3️⃣ get()
Optional에 저장된 값을 반환합니다.
값이 없으면 NoSuchElementException이 발생하므로, 주의해서 사용해야 합니다.
Optional<String> optional = Optional.of("Hello");
String value = optional.get(); // "Hello"
4️⃣ orElse(T other)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 기본값을 반환합니다.
Optional<String> optional = Optional.empty();
String value = optional.orElse("Default"); // "Default"
5️⃣ orElseGet(Supplier<? extends T> supplier)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 람다 표현식이나 메서드를 통해 기본값을 제공할 수 있습니다.
Optional<String> optional = Optional.empty();
String value = optional.orElseGet(() -> "Generated Default"); // "Generated Default"
6️⃣ orElseThrow(Supplier<? extends X> exceptionSupplier)
Optional에 값이 있으면 그 값을 반환하고, 값이 없으면 지정한 예외를 던집니다.
Optional<String> optional = Optional.empty();
String value = optional.orElseThrow(() -> new IllegalArgumentException("Value not found")); // 예외 발생
7️⃣ map(Function<? super T, ? extends U> mapper)
Optional에 값이 있으면, 해당 값에 대해 함수형 변환을 적용한 결과를 새로운 Optional로 변환합니다.
값이 없으면 빈 Optional을 반환합니다.
Optional<String> optional = Optional.of("Hello");
Optional<Integer> length = optional.map(String::length); // "Hello"의 길이인 5를 포함한 Optional 반환
8️⃣ flatMap(Function<? super T, Optional<U>> mapper)
map()과 유사하지만, 중첩된 Optional을 평평하게 만듭니다.
Optional<Optional<T>> 형태가 아닌 Optional<T>를 반환합니다.
Optional<String> optional = Optional.of("Hello");
Optional<Integer> length = optional.flatMap(val -> Optional.of(val.length()));
4️⃣ Optional 사용 예시.
1️⃣ Optional을 이용한 안전한 null 처리.
public Optional<User> findUserById(Long id) {
User user = userRepositoty.findById(id);
return Optional.ofNullable(user);
}
public void printUserName(Long userId) {
Optional<User> userOpt = findUserById(userId);
userOpt.ifPresent(user -> System.out.println(user.getName()));
}
2️⃣ 기본값 제공.
Optional<String> optional = Optional.empty();
String value = optional.orElse("Default Value"); //"Default Value" 반환
5️⃣ Optional을 사용하지 않아야 할 경우.
컬렉션 타입
Optional을 사용하여 빈 리스트나 배열을 표현하기보다는, 단순히 빈 컬렉션을 반환하는 것이 더 일반적이고 권장됩니다.
필드 선언
클래스의 필드로 Optional을 사용하는 것은 메모리 오버헤드가 발생할 수 있어 권장하지 않습니다.
Optional은 반환 타입으로 사용하는 것이 좋습니다.
필수 값
절대 null이 되어서는 안 되는 필드나 매개변수에 Optional을 사용할 필요는 없습니다.
6️⃣ 요약.
Optional은 null 값으로 인한 문제를 예방하고, 안전하게 값을 처리할 수 있도록 설계된 클래스입니다.
값이 있을 수도, 없을 수도 있는 상황을 더 명확하게 표현하며, 다양한 메서드를 통해 안전하게 값의 여부를 확인하고 처리할 수 있게 도와줍니다.
이를 통해 코드의 가독성과 유지보수성이 크게 향상됩니다.
-
💾 [CS] 프로그램의 실행 원리에 대한 이해도가 개발자에게 중요한 이유는 무엇일까요?
💾 [CS] 프로그램의 실행 원리에 대한 이해도가 개발자에게 중요한 이유는 무엇일까요?
프로그램의 실행 원리에 대한 이해도는 개발자에게 매우 중요한데, 이는 소프트웨어 개발의 기초적인 부분이자, 효율적인 문제 해결과 최적화된 코드 작성을 가능하게 하기 때문입니다.
프로그램의 실행 원리를 이해하면, 개발자는 단순히 코드를 작성하는 것을 넘어, 프로그램이 어떻게 동작하고, 어디에서 성능 문제나 오류가 발생할 수 있는지 예측하고 해결할 수 있게됩니다.
1️⃣ 효율적인 문제 해결.
프로그램이 실행되는 방식에 대한 이해는 버그를 찾고 해결하는 데 매우 중요합니다.
실행 흐름을 이해하면, 프로그램에서 어떤 부분이 잘못되었는지, 예상치 못한 동작이 왜 발생하는지를 더 쉽게 파악할 수 있습니다.
예를 들어, 메모리 관리, 스택과 힙의 동작 원리, 함수 호출 방식 등을 알고 있으면 메모리 누수나 스택 오버플로와 같은 문제를 신속하게 해결할 수 있습니다.
📝 스택 오버플로(Stack Overflow)
프로그램이 사용 가능한 스택 메모리 영역을 초과하여 더 이상 데이터를 쌓을 수 없게 되는 현상을 말합니다.
이로 인해 프로그램이 비정상적으로 종료되거나 시스템 에러가 발생하게 됩니다.
📝 스택(Stack)
프로그램 실행 시 함수 호출, 로컬 변수 등을 저장하는 메모리 여역입니다.
스택은 후입선출(LIFO: Last In, First Out) 방식으로 작동하며, 함수가 호출될 때마다 해당 함수의 정보(예: 매개변수, 로컬 변수, 반환 주소 등)가 스택에 “쌓이고(push)”, 함수가 종료되면 스택에서 “제거(pop)”됩니다.
2️⃣ 성능 최적화.
프로그램의 실행 원리를 잘 알면, 어디에서 성능 문제가 발생하는지 이해할 수 있습니다.
예를 들어, CPU 사용률, 메모리 사용량, 입출력(I/O) 성능과 같은 부분에서 병목 현상이 발생할 수 있습니다.
프로그램이 실행되는 원리를 알면, 이런 성능 문제를 분석하고, 더 나은 성능을 얻기 위해 코드를 최적화할 수 있습니다.
알고리즘의 시간 복잡도와 공간 복잡도를 고려하고, 메모리 할당과 해제가 프로그램 성능에 미치는 영향을 이해하면, 더 효율적인 코드를 작성할 수 있습니다.
📝 시간 복잡도(Time Complexity)
알고리즘이 입력 크기에 따라 실행하는 연산의 수를 측정하는 지표로, 알고리즘의 효율성을 평가하는 데 사용됩니다.
시간 복잡도(Time Complexity)는 주로 입력 크기(n)가 커질수록 알고리즘의 실행 시간이 얼마나 빨리 증가하는지를 나타내며, 빅-오 표기법(Big-O Notaiton)으로 표현됩니다.
📝 공간 복잡도(Space Complexity)
알고리즘이 실행되는 동안 사용하는 메모리 공간의 양을 측정하는 지표입니다.
시간 복잡도(Time Complexity)가 알고리즘의 실행 시간을 평가하는 것이라면, 공간 복잡도(Space Complexity)는 알고리즘이 얼마나 많은 메모리를 사용하는지를 평가하는 개념입니다.
3️⃣ 디버깅과 오류 처리 능력 향상.
프로그램이 실행되는 원리를 이해하면 디버깅이 훨씬 쉬워집니다.
프로그램에서 예외(Exception)나 에러(Error)가 발생할 때, 에러 메시지나 스택 트레이스를 통해 프로그램이 어떻게 실행되다가 멈췄는지 파악할 수 있습니다.
개발자는 이 정보를 기반으로 문제의 원인을 정확히 찾고 해결할 수 있습니다.
예를 들어, 멀티스레드 프로그램에서 동시성 문제(데드락, 레이스 컨디션 등)가 발생하는 이유와 이를 해결하기 위해 락(Lock)이나 동기화(Synchronization)를 어떻게 사용하는지를 이해하는 것이 중요합니다.
📝 스택 트레이스(Stack Trace)
프로그램 실행 중 예외(Exception)가 발생했을 때, 예외가 발생한 지점과 그 예외로 이어진 함수 호출 경로를 보여주는 디버깅 정보입니다.
이를 통해 개발자는 어떤 예외가 어디서 발생했는지, 그리고 그 예외가 어떻게 발생했는지를 파악할 수 있습니다.
4️⃣ 최적화된 메모리 관리.
메모리 구조와 프로그램의 메모리 사용 방식을 이해하는 것은 메모리 효율을 높이고, 메모리 누수를 방지하기 위해 중요합니다.
예를 들어, 스택(Stack)과 힙(Heap)의 차이, 객체의 생성과 소멸 과정, 가비지 컬렉션(Garbage Collection)의 동작 방식 등을 알고 있으면, 메모리 사용향을 최적화할 수 있습니다.
특히, 언어마다 메모리 관리 방식이 다르기 때문에, 각 언어의 메모리 관리 특성을 이해하고 효율적으로 활용하는 것이 필요합니다.
📝 가비지 컬렉션(Garbage Collection)
더 이상 사용되지 않는 객체를 메모리에서 자동으로 해제하는 메모리 관리 기법입니다.
자바(Java)와 같은 프로그래밍 언어에서, 개발자가 수동으로 메모리를 해제하지 않아도 가비지 컬렉터(Garbage Collector)가 자동으로 불필요한 객체를 감지하고 메모리를 회수하여 메모리 누수(Memory Leak)를 방지합니다.
5️⃣ 프로그램 설계와 구조화.
프로그램이 어떻게 실행되는지에 대한 이해는 좋은 설계와 구조화된 프로그램을 작성하는 데 필수적입니다.
프로그램을 작성할 때는 모듈화, 객체지향 설계, 함수형 프로그래밍 등의 개념을 사용해 코드를 작성하게 되는데, 이러한 개념이 실제 프로그램 실행 시 어떻게 적용되고, 효율성에 어떤 영향을 미치는지 이해하면 더 나은 프로그램을 설계할 수 있습니다.
데이터 흐름, 의존성 관리, 캡슐화 등의 개념을 잘 활용하면 유지보수하기 쉬운 프로그램을 만들 수 있습니다.
📝 모듈화(Modularization)
소프트웨어 개발에서 프로그램을 독립적이고 재사용 가능한 작은 단위(모듈, Module)로 나누는 설계 기법입니다.
각 모듈(Module)는 특정 기능을 수행하며, 다른 모듈(Module)과 명확하게 정의된 인터페이스(Interface)를 통해 상호작용합니다.
모듈화(Modularization)의 목적은 코드의 가독성, 유지보수성, 재사용성을 높이고, 복잡한 시스템을 더 쉽게 관리할 수 있도록 하는 것 입니다.
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
🙋♂️ 소프트웨어 공학에서의 모듈.
📝 객체지향 설계(Object-Oriented Design, OOD)
객체지향 프로그래밍(Object-Oriented Programming, OOP)의 원칙과 개념을 바탕으로, 프로그램을 객체라는 독립적인 단위로 설계하는 방법을 의미합니다.
객체지향 설계(Object-Oriented Design, OOD)는 프로그램의 구성 요소를 클래스와 객체로 나누고, 이들 간의 관계를 정의하여 효율적이고 재사용 가능하며 유지보수하기 쉬운 소프트웨어를 만드는 것을 목표로 합니다.
📝 함수형 프로그래밍(Function Programming, FP)
함수를 기본 구성 요소로 사용하는 프로그래밍 패러다임입니다.
함수형 프로그래밍에서는 순수 함수와 데이터의 불변성을 강조하며, 부작용(Side Effect)을 피하는 것을 목표로 합니다.
이를 통해 코드의 가독성, 유지보수성, 재사용성을 높이고, 병렬 처리와 같은 상황에서도 안정적이고 예측 가능한 코드를 작성할 수 있습니다.
6️⃣ 컴퓨터 시스템의 이해.
프로그램의 실행 원리를 이해하기 위해서는 컴퓨터 시스템 자체의 동작 방식에 대한 이해도 중요합니다.
CPU 연산, 메모리 접근, 캐시 메모리, 파일 시스템, 네트워크 통신 등의 개념을 잘 이해하고 있으면, 프로그램이 컴퓨터 시스템에서 어떻게 실행되고, 어떤 부분에서 성능 문제나 오유가 발생할 수 있는지를 파악할 수 있습니다.
예를 들어, I/O 바운드 작업(입출력)과 CPU 바운드 작업(연산)의 차이를 이해하면, 각기 다른 상황에서 어떤 최적화가 필요한지 알 수 있습니다.
📝 I/O 바운드 작업(I/O-Bound Task)
입출력(Input/Output) 작업의 속도에 의해 전체 작업의 성능이 제한되는 작업을 의미합니다.
즉, 프로그램이 실행되는 동안 CPU가 데이터를 처리하는 것보다, 데이터를 읽고 쓰는 작업(I/O)에서 더 많은 시간이 소비되는 상황을 말합니다
📝 CPU 바운드 작업(CPU-Bound Task)
프로그램의 실행 속도가 CPU의 처리 속도에 의해 제한되는 작업을 의미합니다.
즉, 연산이나 계산 작업이 많아서 CPU가 대부분의 시간을 계산 처리에 사용하며, CPU의 성능이 전체 작업의 성능을 결정짓는 상황입니다.
7️⃣ 더 나은 소프트웨어 설계.
프로그램의 실행 원리를 이해하면, 객체지향 프로그래밍(Object-Oriented Programming, OOP), 함수형 프로그래밍(Function Programming, FP), 동시성 프로그래밍(Concurrency Programming) 등 다양한 프로그래밍 패러다임을 이해하고 이를 적절하게 활용할 수 있습니다.
🙋♂️객체 지향 프로그래밍(Object-Oriented Programming, OOP)는 무엇일까요?
📝 동시성 프로그래밍(Concurrency Programming)
여러 작업을 동시에 수행할 수 있도록 프로그램을 설계하는 기법입니다.
동시성(Concurrency)은 작업의 실행 흐름을 겹치게 하거나 병렬로 처리하여, 시스템의 효율성을 높이고 처리 시간을 줄이는데 중점을 둡니다
동시성 프로그래밍(Concurrency Programming)은 멅티스레딩, 멀티프로세싱을 포함하여, 비동기 프로그래밍과 같은 다양한 기법을 사용하여 시스템 자원을 최대한 활용할 수 있도록 합니다.
📝 멀티스레딩(Multithreading)
하나의 프로세스 내에서 여러 스레드(Thread)를 동시에 실행하여, 병렬로 작업을 수행할 수 있도록 하는 프로그래밍 방식입니다.
스레드(Thread)는 프로세스 내의 실행 단위로, 각 스레드(Thread)는 독립적으로 실행되며, 같은 메모리 공간을 공유합니다.
멀티스레딩(MultiThreading)을 통해 프로그램의 성능을 향상시키고, 동시성 작업을 효율적으로 처리할 수 있습니다.
📝 동시성 작업(Concurrency)
여러 작업이 동시에 실행되는 것처럼 보이도록 처리하는 방식을 말합니다.
실제로는 작업들이 시간을 나눠가면서 번갈아 가며 실행되지만, 사용자는 마치 여러 작업이 동시에 수행되는 것처럼 느낄 수 있습니다.
동시성(Concurrency)은 시스템의 자원을 효율적으로 사용하고, 작업의 처리 속도를 높이는 데 중요한 개념입니다.
📝 멀티 프로세싱(Multiprocessing)
여러 개의 프로세스를 동시에 실행하여 작업을 병렬로 처리하는 방식을 말합니다.
멀티 프로세싱은 멀티코어 CPU 환경에서 각 프로세스가 독립적인 메모리 공간을 사용하면서, 실제로 동시에 작업을 수행할 수 있게 해줍니다.
이 방식은 병렬성(Parallelism)을 활용하여 프로그램의 성능을 극대화할 수 있습니다.
📝 비동기 프로그래밍(Asynchornous Programming)
작업을 실행할 때 그 작업이 완료될 때까지 기다리지 않고, 다른 작업을 계속 수행할 수 있도록 하는 프로그래밍 방식입니다.
비동기 프로그래밍(Asynchornous Programming)을 통해 I/O 작업, 네트워크 요청, 파일 읽기/쓰기, 데이터베이스 조회 등의 대기 시간이 긴 작업을 처리하는 동안 프로그램이 멈추지 않고 다른 작업을 병렬로 수행할 수 있습니다.
이를 통해 프로그램의 응답성을 높이고 자원 효율성을 극대화할 수 있습니다.
📝 프로세스(Process)
실행 중인 프로그램의 인스턴스(Instance)로, 운영체제(Operating System, OS)가 프로그램을 실행할 때 생성되는 독립적인 작업 단위입니다.
프로세스(Process)는 코드(Code), 데이터(Data), 메모리(Memory), CPU 자원(CPU Resource)을 할당받아 프로그램의 명령을 수행하며, 각각의 프로세스(Process)는 서로 독립적인 메모리 공간을 가지고 실행됩니다.
8️⃣ 예시: Java 프로그램의 실행 원리 이해의 중요성.
Java 프로그램을 예로 들어보겠습니다.
Java의 메모리 관리.
Java는 가비지 컬렉션(Garbage Collection)을 통해 메모리를 자동ㅇ로 관리하지만, 가비지 컬렉션(Garbage Collection)의 동작 방식과 메모리 영역(Heap, Stack, Metaspace 등)에 대한 이해가 있어야 메모리 사용을 최적화할 수 있습니다.
📝 Metaspace
Java 8부터 도입된 메모리 영역으로, 클래스 메타데이터(Class Metadata)를 저장하는 공간입니다.
Java 8 이전에는 Permanent Generation(영구 영역, PermGen)이라는 공간에 클래스 메타데이터를 저장했지만, 이를 개선하기 위해 Metaspace로 대체되었습니다.
멀티스레드와 동시성(Multithread and Concurrency)
Java에서 멀티스레드(Multithread) 프로그램을 작성할 때, 스레드(Thread)의 실행 순서, 동기화, 스레드 풀 등의 개념을 이해하지 못하면 데드락(Deadlock)이나 레이스 컨디션(Race Condition) 같은 문제가 발생할 수 있습니다.
📝 스레드 풀(Thread Pool)
미리 생성된 스레드들의 모음으로, 필요할 때마다 작업(task)을 할당할 수 있도록 재사용 가능한 스레드를 관리하는 구조입니다.
스레드 풀(Thread Pool)을 사용하면 스레드 생성과 소멸에 드는 비용을 줄이고, 시스템 자원을 효율적으로 사용할 수 있게 합니다.
📝 데드락(Deadlock)
두 개 이상의 프로세스(Process)나 스레드(Thread)가 서로 다른 자원을 기다리며 무한히 대기하는 상태를 말합니다.
데드락(Deadlock)이 발생하면 관련된 프로세스(Process)나 스레드(Thread)들은 모두 정지되며, 작업을 진행할 수 없는 상태에 빠지게 됩니다.
데드락(Deadlock)은 동시성 프로그래밍(Concurrency Programming)에서 발생할 수 있는 심각한 문제로, 특히 멀티스레딩(Multithreading) 환경에서 자주 나타납니다.
예를 들어, 두 스레드가 서로 상대방이 보유한 자원을 필요로 할 때, 두 스레드는 영원히 자원을 얻지 못한 채 대기하게 됩니다.
📝 레이스 컨디션(Race Condition)
두 개 이상의 스레드 또는 프로세스가 동시에 공유 자원에 접근하고, 그 결과가 실행 순서에 따라 달라지는 상황을 말합니다.
즉, 동시성 프로그래밍(Concurrency Programming)에서 스레드(Thread)들이 경쟁적으로 자원에 접근할 때 발생하는 문제로, 프로그램의 의도치 않은 결과를 초래할 수 있습니다.
레이스 컨디션은 특히 공유 자원(변수, 데이터 구조, 파일 등)에 대해 읽기와 쓰기 작업이 동시에 이루어지는 경우에 발생하며, 동기화가 제대로 이루어지지 않으면 데이터의 일관성이 깨지게 됩니다.
JVM(Java Virtual Machine)의 작동 방식
JVM(Java Virtual Machine) 의 실행 원리를 이해하면, 프로그램의 성능을 튜닝하고, 메모리 문제를 해결하며, 실행 환경을 최적화할 수 있습니다.
9️⃣ 요약.
프로그램의 실행 원리에 대한 이해는 효율적인 문제 해결, 성능 최적화, 디버깅 능력 향상, 설계 능력 향상 등 여러 측면에서 개발자에게 필수적입니다.
이를 통해 더 나은 품질의 소프트웨어를 개발할 수 있으며, 복잡한 문제를 해결할 때 더 나은 접근 방식을 찾을 수 있습니다.
프로그램이 어떻게 실행되고, 메모리와 CPU 자원을 어떻게 사용하는지를 이해하는 것은 단순히 코드를 작성하는 것을 넘어서, 전방적인 소프트웨어 품질을 높이는 데 핵심적인 역할을 합니다.
-
🍃[Spring] Spring Data JPA란 무엇일까요?
🍃[Spring] Spring Data JPA란 무엇일까요?
Spring Data JPA는 Spring Framework의 일부로, JPA(Java Persistence API)를 더욱 쉽게 사용할 수 있도록 도와주는 모듈(Module)입니다.
JPA(Java Persistence API)는 자바 애플리케이션에서 객체와 관계형 데이터베이스(Relational Database, RDB) 간의 매핑을 제공하는 표준 인터페이스인데, Spring Data JPA는 이를 기반으로 개발자가 더 적은 코드로 데이터베이스와 상호작용할 수 있게 도와줍니다.
1️⃣ Spring Data JPA의 주요 특징.
1️⃣ 간편한 데이터 엑세스 계층.
Spring Data JPA는 데이터베이스와의 CRUD(Create, Read, Update, Delete) 작업을 손쉽게 구현할 수 있는 방법을 제공합니다.
개발자는 복잡한 SQL 쿼리를 직접 작성할 필요 없이, Repository 인터페이스를 정의하는 것만으로 데이터베이스 작업을 처리할 수 있습니다.
2️⃣ 자동 구현 Repository.
Spring Data JPA는 Repository 인터페이스를 정의하면, 런타임 시점에 해당 인터페이스의 구현체를 자동으로 생성합니다.
예를 들어, findById, save, delete와 같은 기본적인 데이터베이스 연산은 별도로 구현할 필요가 없습니다.
3️⃣ 메서드 이름 기반 쿼리 생성.
Spring Data JPA는 메서드 이름을 분석하여 자동으로 쿼리를 생성합니다.
예를 들어, findByName과 같은 메서드를 정의하면, name 필드를 기준으로 데이터를 조회하는 쿼리를 자동으로 생성합니다.
이를 통해 복잡한 쿼리도 쉽게 작성할 수 있으며, 추가적인 코드 작성이 줄어들어 개발 속도를 높여줍니다.
4️⃣ JPQL 및 네이티브 쿼리 지원.
필요할 경우 JPQL(Java Persistence Query Language) 또는 네이티브 SQL(Structured Query Language) 쿼리를 직접 작성할 수도 있습니다.
복잡한 쿼리를 처리하거나 성능 최적화가 필요한 경우 유용하게 사용할 수 있습니다.
5️⃣ 페이징 및 정렬 지원.
Spring Data JPA는 데이터 조회 시 페이징(Paging)과 정렬(Sorting) 기능을 기본적으로 지원합니다.
이를 통해 대량의 데이터를 효율적으로 처리할 수 있습니다.
2️⃣ 주요 구성 요소.
1️⃣ Entity 클래스.
데이터베이스 테이블과 매핑되는 자바 클래스입니다.
JPA(Java Persistence API) 엔티티(Entity) 어노테이션(@Entity)을 사용하여 테이블 구조를 정의합니다.
2️⃣ Repotitory 인터페이스.
Spring Data JPA에서 데이터베이스에 접근하기 위해 사용하는 인터페이스입니다.
CrudRepository, JpaRepository, PagingAndSortingRepository와 같은 다양한 Repository 인터페이스가 제공됩니다.
이를 확장하여 기본적인 CRUD(생성, 조회, 수정, 삭제) 기능뿐만 아니라 페이징, 정렬 등을 쉽게 구현할 수 있습니다.
3️⃣ Spring Data JPA의 어노테이션
@Entity: 클래스를 JPA 엔티티(Entity)로 정의합니다.
@Id: 기본 키(Primary Key)를 지정합니다.
@Repository: 데이터 접근 객체(DAO)로 사용될 인터페이스 또는 클래스에 사용합니다. Spring에서 해당 클래스를 빈으로 관리할 수 있도록 합니다.
@Query: JPQL 또는 네이티브 쿼리를 직접 작성할 때 사용합니다.
🙋♂️ API에서의 인터페이스와 소프트웨어 공학에서의 인터페이스 개념.
🙋♂️ JPA 어노테이션 - @Entity
🙋♂️ JPA 어노테이션 - @Id
🙋♂️ 언제 @Service,@Repository,@Controller와 같은 어노테이션을 사용할까?
🙋♂️ 엔티티(Entity)는 무엇일까요?
3️⃣ Spring Data JPA 사용 예시.
1️⃣ 엔티티 클래스 정의.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneretedValue;
import javax.persistence.GenerationType;
@Entity
public class User {
@Id
@GeneretedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// 기본 생성자, getter, setter 생략
}
2️⃣ Repository 인터페이스 정의.
import org.springframework.data.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 CRUD 메서드 외에 추가 메서드 정의
User findByName(String name);
}
위 코드에서 UserRepository는 JpaRepository를 확장하여, User 엔티티(Entity)와 Long 타입의 기본 키(Primary Key)를 사용하는 Repository로 정의됩니다.
findByName: 메서드 이름을 기반으로 Spring Data JPA가 name 필드로 User를 찾는 쿼리를 자동으로 생성합니다.
3️⃣ Service 클래스에서 사용.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserByName(String name) {
return userRepository.findByName(name);
}
public User createUser(User user) {
return userRepository.save(user);
}
}
4️⃣ Spring Boot 설정(application.properties)
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
4️⃣ Spring Data JPA의 장점.
1️⃣ 개발 생산성 향상.
데이터 접근 코드를 줄여주며, 메서드 이름 기반의 쿼리 생성으로 개발 속도를 높입니다.
2️⃣ 유연성.
복잡한 쿼리를 직접 작성할 수 있으며, 다양한 기능을 쉽게 확장할 수 있습니다.
3️⃣ Spring 생태계와 통합.
Spring Framework와 쉽게 통합되며, Spring Boot를 통해 설정이 간편해집니다.
4️⃣ 페이징 및 정렬 지원.
대량의 데이터를 효율적으로 처리할 수 있도록 기본 페이징과 정렬을 제공합니다.
5️⃣ 요약.
Spring Data JPA는 JPA 기반의 애플리케이션 개발을 더욱 쉽게 할 수 있도록 지원하는 Spring 모듈입니다.
Repository 인터페이스를 통해 기본적인 CRUD(생성, 조회, 수정, 삭제) 기능과 메서드 이름 기반의 쿼리를 제공하며, 개발자가 데이터베이스와의 상호작용 코드를 줄일 수 있도록 도와줍니다.
이를 통해 빠르고 간편하게 데이터베이스 접근 계층을 구현할 수 있으며, 더 복잡한 쿼리가 필요한 경우 직접 쿼리를 작성할 수도 있습니다.
-
🍃[Spring] Repository 패턴이란 무엇일까요?
🍃[Spring] Repository 패턴이란 무엇일까요?
Repository 패턴은 데이터 접근 로직을 비즈니스 로직(Business Logic)과 분리하여, 데이터베이스나 다른 저장소와의 상호작용을 캡슐화하는 디자인 패턴입니다.
이 패턴을 사용하면, 애플리케이션의 나머지 부분이 데이터베이스와 직접 상호작용하는 대신 Repository를 통해 데이터를 저장, 조회, 수정, 삭제 등의 작업을 수행할 수 있게 되어, 코드의 유지보수성과 재사용성을 높일 수 있습니다.
1️⃣ Repository 패턴의 주요 개념.
1️⃣ 데이터 접근 로직의 캡슐화.
데이터베이스와의 상호작용을 Repository 클래스로 감싸서 구현합니다.
이렇게 하면 애플리케이션의 다른 부분에서 데이터베이스와 직접 상호작용하지 않고, Repository 클래스를 통해서만 데이터를 다루게 됩니다.
이로 인해 데이터 접근 로직과 비즈니스 로직이 분리되어, 코드의 유지보수성이 높아집니다.
예를 들어, 데이터베이스를 변경해야 하는 경우에도 비즈니스 로직에는 영향을 미치지 않고 Repository만 수정하면 됩니다.
2️⃣ 데이터 소스의 추상화.
Repository 패턴은 데이터가 어디에서 오는지에 대해 추상화를 제공합니다.
데이터가 관계형 데이터베이스(Relational Database, RDB), NoSQL 데이터베이스, 파일 시스템, 웹 서비스 등 어떤 곳에 저장되어 있는지와 무관하게 동일한 인터페이스를 통해 데이터를 다룰 수 있습니다.
이 추상화 덕분에, 애플리케이션은 특정 데이터 소스에 종속되지 않으며, 데이터 소스를 교체하거나 확장하기 쉬워집니다.
3️⃣ CRUD 작업의 표준화.
Repository는 보통 CRUD 작업(Create, Read, Update, Delete)을 표준화된 방식으로 제공합니다.
이를 통해 데이터 접근 로직이 일관성을 유지할 수 있으며, 개발자가 CRUD(생성, 조회, 수정, 삭제) 작업을 수행할 때 혼란 없이 사용할 수 있습니다.
2️⃣ Repository 패턴의 장점.
1️⃣ 비즈니스 로직(Business Logic)과 데이터 접근 로직의 분리.
데이터 접근 코드와 비즈니스 로직(Business Logic) 코드가 분리되므로, 유지보수가 쉬워집니다.
데이터베이스 관련 로직이 변경되더라도 비즈니스 로직(Business Logic)에 영향을 주지 않습니다.
🙋♂️ 비즈니스 로직(Business Logic)이란?
2️⃣ 데이터 소스의 변경에 대한 유연성.
애플리케이션은 특정 데이터베이스 기술이나 저장소에 의존하지 않습니다.
예를 들어, 애플리케이션이 MySQL에서 MongoDB로 데이터베이스를 전환해야 할 경우, Repository 내부의 구현만 변경하면 되므로 변경 작업이 용이해집니다.
3️⃣ 테스트 용이성.
데이터 접근 로직이 Repository로 추상화되어 있기 때문에, 테스트 코드에서 Mock(모의 객체)를 사용해 Repository의 동작을 대체할 수 있습니다.
이를 통해 데이터베이스 없이도 비즈니스 로직을 테스트할 수 있게 됩니다.
3️⃣ Spring Data JPA에서의 Repository 패턴 구현.
Spring Data JPA는 Repository 패턴을 편리하게 구현할 수 있도록 해줍니다.
개발자는 데이터 접근 로직을 일일이 작성할 필요 없이, Spring Data JPA가 제공하는 JpaRepository 인터페이스를 확장함으로써 자동으로 CRUD(생성, 조회, 수정, 삭제) 작업을 처리할 수 있게 됩니다.
👉 예시: User 엔티티 클래스.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String Integer age;
// 기본 생성자, getter, setter 생략
}
👉 예시: UserRepository 인터페이스.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
}
위의 코드에서 UserRepository는 JpaRepository를 상속받아 자동으로 기본적인 CRUD 메서드(save, findAll, findById, delete 등)를 사용할 수 있습니다.
또한, findByName 이라는 메서드를 선언하면, Spring Data JPA는 이 메서드 이름을 분석하여 name 필드를 기준으로 조회하는 SQL 쿼리를 자동으로 생성해줍니다.
👉 서비스 레이어에서 사용.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.streotype.Service;
@Service
public class UserSevice {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserByName(String name) {
return userRepository.findByName(name);
}
public void saveUser(User user) {
userRepository.save(user);
}
}
4️⃣ Repository 패턴의 사용 예시.
만약 현재 MySQL을 사용하고 있지만, 미래에 MongoDB로 전환하고 싶다고 가정해 보겠습니다.
Repository 패턴을 사용하고 있다면, 다음과 같은 작업이 가능합니다.
MySQL 기반의 UserRepository 구현을 MongoDB 기반의 새로운 UserRepository로 교체.
서비스 레이어나 비즈니스 로직을 수정할 필요 없이, 데이터베이스 기술을 전환할 수 있음.
5️⃣ 요약.
Repository 패턴은 데이터베이스와의 상호작용을 캡슐화하여, 데이터 접근 로직을 비즈니스 로직과 분리하는 디자인 패턴입니다.
이 패턴은 데이터 소스의 변경에 대한 유연성을 제공하고, 애플리케이션 코드의 유지보수성과 재사용성을 높여줍니다.
Spring Data JPA는 이러한 Repository 패턴을 매우 쉽게 구현할 수 있도록 다양한 기능을 제공하여, 개발자들이 데이터 접근 계층을 간단하고 효율적으로 관리할 수 있게 해줍니다.
-
🍃[Spring] Spring Data JPA는 라이브러리가 아닌 모듈인가요?
🍃[Spring] Spring Data JPA는 라이브러리(Library)가 아닌 모듈(Module)인가요?
Spring Data JPA는 Spring Data 프로젝트의 하위 모듈(Module)로, 이를 모듈(Module)이라고 부르는 것이 더 정확합니다.
다만, 라이브러리(Library)라는 용어와도 종종 혼용되어 사용되곤 합니다.
두 용어 간에 약간의 차이가 있지만, 개발자들이 Spring Data JPA를 이야기할 때는 주로 모듈(Module)로서의 의미를 강조합니다.
1️⃣ 모듈(Module)과 라이브러리(Library)의 차이.
1️⃣ 모듈(Module)
모듈(Module)은 특정 기능이나 목적을 중심으로 구성된 컴포넌트를 의미하며, 더 큰 시스템의 일부분으로 동작합니다.
Spring Data JPA는 Spring Data라는 더 큰 프로젝트의 일부분이며, 데이터 접근 계층에서 JPA(Java Persistence API)와 Hibernate를 쉽게 사용할 수 있도록 하는 기능을 제공합니다.
이 점에서 Spring Data JPA는 모듈(Module)로 볼 수 있습니다.
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
🙋♂️ 소프트웨어 공학에서의 컴포넌트.
🙋♂️ 소프트웨어 공학에서의 모듈.
2️⃣ 라이브러리(Library)
라이브러리(Library)는 재사용 가능한 코드 집합으로, 개발자가 특정 기능을 쉽게 구현할 수 있도록 도와줍니다.
Spring Data JPA도 의존성으로 추가하면 프로젝트에서 사용 가능한 코드 집합을 제공하기 때문에 라이브러리(Library)로 볼 수도 있습니다.
하지만, 더 큰 맥락에서 모듈(Module)은 더 큰 시스템의 일부로 동작하는 반면, 라이브러리(Library)는 독립적으로 재사용 가능한 코드 집합이라는 점에서 차이가 있습니다.
🙋♂️ 라이브러리(Library)와 프레임워크(Framework)의 차이점.
2️⃣ Spring Data 프로젝트의 구조.
Spring Data는 데이터 접근을 단순화하기 위해 만들어진 프로젝트로, 다양한 데이터 저장소에 쉽게 접근할 수 있도록 여러 모듈(Module)로 구성되어 있습니다.
이 프로젝트는 다양한 모듈(Module)을 포함하고 있으며, 각 모듈(Module)은 특정 데이터 저장소에 대한 기능을 제공합니다.
예를 들어
Spring Data JPA : JPA를 사용한 관계형 데이터베이스 접근을 위한 모듈
Spring Data MongoDB : MongoDB를 사용한 NoSQL 데이터베이스 접근을 위한 모듈
Spring Data Redis : Redis 데이터 저장소 접근을 위한 모듈
Spring Data Elasticsearch : Elasticsearch 접근을 위한 모듈
이처럼 Spring Data JPA는 Spring Data 프로젝트의 여러 모듈 중 하나로, JPA(Java Persistence API)를 기반으로 하는 관계형 데이터베이스(Relational Database)와의 상호작용을 단순화하는 기능을 제공하는 역할을 합니다.
3️⃣ Spring Data JPA를 모듈(Module)로 보는 이유.
Spring 생태계의 일부분
Spring Data JPA는 Spring Data 프로젝트의 하위 모듈로, Spring의 일관된 구조와 스타일을 따르며, 다른 Spring 모듈과 자연스럽게 통합됩니다.
특정 기능에 집중
Spring Data JPA는 JPA를 통해 데이터베이스와 상호작용하는 기능에 특화되어 있으며, 이러한 역할에 초점을 맞춘 독립적인 모듈로 설계되었습니다.
의존성 관리
Maven 또는 Gradle의 의존성으로 추가할 때, Spring Data JPA 모듈을 추가함으로써 JPA 기반 데이터 접근 계층을 쉽게 구현할 수 있습니다.
4️⃣ 요약.
Spring Data JPA는 Spring Data 프로젝트의 하위 모듈로서, JPA 기반의 데이터 접근을 쉽게 할 수 있도록 도와주는 기능을 제공합니다.
이 모듈은 더 큰 Spring Data 생태계의 일부로 동작하며, JPA를 사용하는 애플리케이션에서 데이터베이스와의 상호작용을 단순화하는 역할을 합니다.
개발자들이 라이브러리와 모듈이라는 용어를 혼용해 사용하기도 하지만, Spring Data JPA는 엄밀히 말하면 모듈로 보는 것이 맞습니다.
-
🍃[Spring] spring.jpa.properties.hibernate.format_sql이란 무엇일까요?
🍃[Spring] spring.jpa.properties.hibernate.format_sql이란 무엇일까요?
spring.jpa.properties.hibernate.format_sql은 Spring Boot 애플리케이션에서 Hibernate가 생성하는 SQL(Structured Query Language) 쿼리를 읽기 쉽게 포맷하는 설정입니다.
이 설정을 활성화하면 Hibernate가 데이터베이스에 실행하는 SQL(Structured Query Language) 쿼리가 포맷된 형식으로 출력되며, 이를 통해 개발자는 쿼리의 구조를 더 쉽게 이해할 수 있습니다.
1️⃣ 역할.
SQL(Structured Query Language) 쿼리 포맷팅
기본적으로 Hibernate가 출력하는 SQL(Structured Query Language) 쿼리는 모두 한 줄로 출력되기 때문에 복잡한 쿼리를 읽기가 어렵습니다.
spring.jpa.properties.hibernate.format_sql을 true로 설정하면 SQL(Structured Query Language) 쿼리를 보기 좋게 들여쓰기가 된 형태로 출력해줍니다.
디버깅 및 최적화
SQL(Structured Query Language) 쿼리가 가독성이 좋아지면, 개발자는 애플리케이션이 데이터베이스에 어떤 쿼리를 실행하는지 쉽게 파악할 수 있으며, 쿼리를 디버깅하거나 성능을 최적화하는 데 도움이 됩니다.
2️⃣ 설정 방법.
Spring Boot에서는 application.properties 또는 application.yml 파일에서 이 속성을 설정할 수 있습니다.
👉 application.properties 파일에서 설정.
spring.jpa.properties.hibernate.format_sql=true
👉 application,yml 파일에서 설정.
spring:
jpa:
properties:
hibernate:
format_sql: true
3️⃣ 동작 예시.
spring.jpa.properties.hibernate,format_sql=true로 설정한 후, 복잡한 SQL 쿼리를 실행하면 콘솔에 출력되는 SQL이 자동으로 들여쓰기가 적용된 상태로 출력됩니다.
👉 설정 전(포맷팅되지 않은 SQL)
select user0_.id as id1_0_, user0_.email as email2_0_, user0_.name as name3_0_ from user user0_
👉 설정 후(포맷팅된 SQL)
select
user0_.id as id1_0_,
user0_.email as email2_0_,
user0_.name as name3_0_
from
user user0_
이렇게 포맷팅된 SQL은 가독성이 높아져, 복잡한 SQL 쿼리도 쉽게 이해할 수 있습니다.
4️⃣ 관련 설정.
spring.jpa.show-sql
이 설정은 Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력하도록 활성화합니다.
show-sql=true로 설정하면 SQL 쿼리를 콘솔에서 확인할 수 있습니다.
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.use_sql_comments
이 설정은 쿼리 로그에 SQL 주석을 추가하여, 쿼리가 실행된 위치나 목적에 대한 추가 정보를 제공합니다.
spring.jpa.properties.hibernate.use_sql_comments=true
5️⃣ 요약.
spring.jpa.properties.hibernate.format_sql은 Hibernate가 출력하는 SQL 쿼리를 읽기 쉽게 포맷팅하는 설정입니다.
이 설정을 true로 활성화하면 SQL 쿼리의 가독성이 향상되며, 개발자는 디버깅과 쿼리 최적화 작업을 더 쉽게 할 수 있습니다.
-
🍃[Spring] spring.jpa.properties.hibernate.dialect이란 무엇일까요?
🍃[Spring] spring.jpa.properties.hibernate.dialect이란 무엇일까요?
spring.jpa.properties.hibernate.dialect는 Spring Boot 애플리케이션에서 Hibernate가 사용하는 SQL(Structured Query Language) 방언(dialect)을 지정하는 설정입니다.
Hibernate Dialect는 Hibernate가 데이터베이스와 상호작용할 때 사용하는 SQL 방언을 정의합니다.
즉, 서로 다른 데이터베이스마다 사용하는 SQL 문법이나 기능이 약간씩 다르기 때문에, Hibernate가 각 데이터베이스의 특성에 맞는 SQL을 생성하도록 돕는 역할을 합니다.
1️⃣ 왜 필요한가요?
서로 다른 데이터베이스는 SQL(Structured Query Language) 문법이나 기능이 조금씩 다릅니다.
예를 들어, MySQL, Oracle, PostgreSQL, SQL Server 등은 일부 SQL 문법이나 함수의 지원 방식이 다를 수 있습니다.
Hibernate는 데이터베이스 독립성을 유지하기 위해 다양한 데이터베이스에 맞춰 동작할 수 있도록 설계되었습니다.
그러나 이를 위해 각 데이터베이스에 맞는 적절한 SQL(Structured Query Language)을 생성해야 하며, 이를 위해 Dialect를 사용합니다.
Hibernate Dialect는 특정 데이터베이스에 맞춰 SQL 쿼리를 최적화하거나, 데이터베이스에 특화된 기능을 사용할 수 있도록 합니다.
2️⃣ 설정 방법.
Spring Boot에서 spring.jpa.properties.hibernate.dialect를 application.properties 또는 application.yml 파일에 설정하여, 사용하는 데이터베이스에 맞는 방언(dialect)을 명시할 수 있습니다.
👉 application.properties 파일에서 설정.
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
👉 application.yml 파일에서 설정.
spring:
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
3️⃣ 대표적인 Hibernate Dialect 예시.
1️⃣ MySQL
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
2️⃣ PostgreSQL
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
3️⃣ Oracle
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
4️⃣ H2(In-Memory Database)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
5️⃣ SQL Server
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServerDialect
4️⃣ 자동 설정.
Spring Boot는 대부분의 경우 spring.datasource.url 속성에 설정된 데이터베이스 URL을 기반으로 적절한 Hibernate Dialect를 자동으로 설정합니다.
그래서 보통 spring.jpa.properties.hibernate.dialect를 명시적으로 설정할 필요는 없습니다.
그러나 특정한 데이터베이스 버전에 맞는 특화된 최적화 기능을 사용하거나, 자동 설정이 제대로 동작하지 않는 경우에는 이 설정을 명시적으로 정의해야 합니다.
5️⃣ Hibernate Dialect의 역할.
1️⃣ SQL 문법 최적화.
특정 데이터베이스에 맞는 SQL 문법을 사용하도록 합니다.
예를 들어, MySQL과 Oracle에서 자동 증가 필드(autoincrement)를 처리하는 방식이 다릅니다.
Hibernate는 이 차이점을 Dialect를 통해 처리합니다.
2️⃣ 데이터 타입 매핑.
각 데이터베이스는 서로 다른 데이터 타입을 지원합니다.
Dialect는 자바의 데이터 타입을 해당 데이터베이스에서 지원하는 데이터타입으로 변환합니다.
3️⃣ 데이터베이스 특화 기능.
일부 데이터베이스는 특정 기능을 제공하며, Dialect는 이러한 기능을 활용할 수 있도록 최적화된 쿼리를 생성합니다.
예를 들어, PostgreSQL의 시퀀스나 MySQL의 LIMIT 절 들이 그 예입니다.
6️⃣ 요약.
spring.jpa.properties.hibernate.dialect는 Hibernate가 사용할 데이터베이스 방언(Dialect)을 정의하는 설정입니다.
각 데이터베이스는 SQL(Structured Query Language) 문법이나 기능이 약간씩 다르기 때문에, Hibernate는 Dialect를 사용해 특정 데이터베이스에 맞는 SQL을 생성합니다.
Spring Boot는 데이터베이스 URL을 통해 자동으로 적절한 Dialect를 설정하려고 시도하지만, 명시적으로 이 설정을 지정해 데이터베이스와 상호작용을 최적화할 수도 있습니다.
-
🍃[Spring] spring.jap.hibernate.ddl-auto 설정 옵션이란 무엇일까요?
🍃[Spring] spring.jap.hibernate.ddl-auto 설정 옵션이란 무엇일까요?
spring.jpa.hibernate.ddl-auto는 Spring Boot에서 JPA(Java Persistence API)와 Hibernate를 사용할 때 데이터베이스 스키마의 생성을 제어하는 설정 옵션입니다.
📝 스키마(Schema)
데이터베이스의 구조를 정의하는 용어로, 데이터베이스에서 데이터가 어떻게 저장되고, 어떻게 조직화되는지를 설명하는 데 사용됩니다.
스키마는 데이터베이스의 논리적 설계를 나타내며, 테이블, 뷰, 인덱스, 트리거, 저장 프로시저, 제약 조건 등과 같은 데이터베이스 객체들이 어떻게 구성되는지를 정의합니다.
ddl-auto에서 “DDL”은 Data Definition Language의 약자로, 데이터베이스의 테이블, 인덱스, 컬럼 등을 정의하는 SQL(Structured Query Language) 명령어입니다.
이 설정은 애플리케이션의 엔티티(Entity) 클래스가 데이터베이스 스키마(Schema)와 일치하도록 자동으로 처리할지 여부를 결정합니다.
🙋♂️ 엔티티(Entity)는 무엇일까요?
1️⃣ 설정 값.
spring.jpa.hibernate.ddl-auto는 다음과 같은 여러 가지 값을 가질 수 있습니다.
각 값은 Hibernate가 애플리케이션 실행 시 데이터베이스 스키마(Schema)에 대해 어떤 동작을 수행할지를 정의합니다.
1️⃣ none
아무 작업도 하지 않음.
Hibernate는 데이터베이스의 테이블 구조를 변경하지 않습니다.
데이터베이스 스키마(Schema)를 수동으로 관리할 때 사용합니다.
2️⃣ validate
엔티티(Entity) 클래스와 데이터베이스 스키마(Schema)가 일치하는지 검증합니다.
애플리게이션이 시작할 때, 엔티티(Entity) 클래스와 데이터베이스 스키마(Schema)가 정확히 일치하는지 확인합니다.
만약 일치하지 않으면 애플리케이션이 예외를 던지며 시작에 실패합니다.
스키마(Schema)가 이미 존재하고, 스키마(Schema)의 변경 없이 엔티티(Entity) 매핑이 올바른지 확인할 때 사용됩니다.
3️⃣ update
엔티티(Entity) 클래스의 변경 사항을 기존 데이터베이스 스키마(Schema)에 반영합니다.
데이터베이스에 이미 존재하는 테이블 구조에 맞추어 필요한 경우 새로운 컬럼(Column,열)을 추가하거나 수정합니다.
그러나 기존 데이터나 구조를 삭제하지는 않습니다.
개발 중 데이터베이스 스키마(Schema)가 자주 변경될 때 유용하지만, 프로덕션 환경에서는 주의해서 사용해야 합니다.
4️⃣ create
애플리케이션 시작 시 기존 데이터베이스의 테이블을 삭제한 후 새로 생성합니다.
모든 기존 데이터는 삭제되며, 엔티티(Entity) 클래스에 정의된 대로 새로운 테이블과 컬럼(Column, 열)을 생성합니다.
개발 초기 단계에서 사용되며, 매번 데이터베이스 스키마를 새로 생성해야 할 때 적합합니다.
5️⃣ create-drop
애플리케이션 시작 시 데이터베이스 스키마를 생성하고, 종료 시 스키마를 삭제합니다.
create와 유사하지만, 애플리케이션이 종료될 때 테이블을 모두 삭제하는 추가 동작이 있습니다.
테스트 환경에서 유용하게 사용할 수 있으며, 개발이 끝나면 프로덕션 환경에서는 사용하지 않는 것이 좋습니다.
2️⃣ 예시.
Spring Boot의 application.properties 또는 application.yml 파일에서 spring.jpa.hibernate.ddl-auto를 설정할 수 있습니다.
1️⃣ application.properties 예시.
spring.jpa.hibernate.ddl-auto=update
application.yml 예시.
spring:
jpa:
hibernate:
ddl-auto: update
위 설정에서는 update를 사용하여 애플리케시연이 시작될 때 Hibernate가 엔티티(Entity) 클래스의 변경 사항을 데이터베이스 스키마(Schema)에 반영하지만, 기존 데이터를 삭제하지 않습니다.
3️⃣ 각 설정 값의 적합한 사용 환경.
none
이미 완전히 설정된 데이터베이스를 사용하고, Hibernate가 스키마(Schema)에 대해 아무런 변경도 하지 않기를 원할 때 사용.
validate
데이터베이스 스키마(Schema)가 이미 준비되어 있고, 엔티티 클래스와 스키마 간의 불일치를 확인하고 싶을 때 사용.
update
개발 중, 데이터베이스 스키마(Schema)를 유지하면서 엔티티(Entity) 클래스의 변경 사항을 반영하고 싶을 때 사용.
create
개발 중, 데이터베이스 스키마(Schema)를 완전히 새로 생성하고 싶을 때 사용.
create-drop
테스트 시, 애플리케이션 실행 중에만 데이터베이스 스키마를 생성하고 종료 시 모든 데이터를 삭제할 때 사용.
4️⃣ 주의 사항.
create와 create-drop은 프로덕션 환경에서 사용하지 않는 것이 좋습니다.
이 값들은 기존 데이터를 삭제하므로, 실 데이터베이스에 사용하면 데이터 손실이 발생할 수 있습니다.
update는 개발 환경에서 유용하지만, 프로덕션 환경에서는 예상치 못한 변경이 발생할 수 있으므로 주의가 필요합니다.
validate는 스키마(Schema) 검증만 수행하므로, 프로덕션 환경에서 스키마(Schema)의 일관성을 확인하는 용도로 사용될 수 있습니다.
5️⃣ 요약.
spring.jpa.hibernate.ddl-auto는 Hibernate가 애플리케시연 시작 시 데이터베이스 스키마를 어떻게 처리할지를 정의하는 설정입니다.
개발, 테스트, 프로덕션 환경에 따라 적절한 값을 선택해 사용할 수 있으며, 이 설정을 통해 데이터베이스 스키마를 자동으로 생성하거나 검증, 업데이트하는 과정을 간편하게 관리할 수 있습니다.
-
💾[Database] 스키마(Schema)란 무엇일까요?
💾[Database] 스키마(Schema)란 무엇일까요?
스키마(Schema)는 데이터베이스의 구조를 정의하는 용어로, 데이터베이스에서 데이터가 어떻게 저장되고, 어떻게 조직화되는지를 설명하는 데 사용됩니다.
스키마(Schema)는 데이터베이스의 논리적 설계를 나타내면, 테이블, 뷰, 인덱스, 트리거, 저장 프로시저, 제약 조건 등과 같은 데이터베이스 객체들이 어떻게 구성되는지를 정의합니다.
1️⃣ 스키마의 주요 요소.
1️⃣ 테이블(Table).
스키마(Schema)에서 가장 기본적인 요소로, 데이터를 행(Row)과 열(Column) 형식으로 저장합니다.
각 테이블은 하나의 엔티티(예: 사용자, 제품 등)를 표현하며, 테이블의 열(Column)은 속성을, 행은 각 개체의 데이터를 나타냅니다.
2️⃣ 컬럼(Column).
테이블의 각 필드를 나타내며, 데이터 유형(정수, 문자열 등)과 제약 조건(예: NOT NULL, UNIQUE)이 설정됩니다.
스키마(Schema)에서 컬럼(Column, 열)의 이름, 데이터 타입, 크기 등을 정의합니다.
3️⃣ 기본 키(Primary Key)
테이블에서 각 행(Row)을 고유하게 식별할 수 있는 특정 열(또는 열의 조합)입니다.
스키마(Schema)에서 기본 키(Primary Key)는 특정 테이블에서 중복되지 않는 값으로 정의됩니다.
4️⃣ 외래 키(Foreign Key)
다른 테이블의 기본 키(Primary Key)와 연결되는 열(Column)로, 두 테이블 간의 관계를 정의하는 데 사용됩니다.
스키마(Schema)에서 외래 키(Foreign Key)는 데이터베이스 테이블 간의 참조 무결성을 유지하도록 합니다.
5️⃣ 인덱스(Index)
검색 성능을 향상시키기 위해 테이블의 특정 열(Column)에 대한 인덱스를 정의합니다.
스키마(Schema)에서 인덱스는 데이터의 효율적인 검색을 위한 추가적인 구조를 나타냅니다.
6️⃣ 뷰(View)
하나 이상의 테이블에서 데이터를 추출하여 보여주는 가상의 테이블입니다.
데이터베이스 내에 물리적으로 저장되지 않고, SQL(Structured Query Language) 쿼리 결과를 통해 생성됩니다.
7️⃣ 제약 조건(Constraints)
데이터 무결성을 보장하기 위해 테이블이나 열(Column)에 적용되는 규칙입니다.
예를 들어, NOT NULL, UNIQUE, CHECK, FOREIGN KEY 등의 제약 조건이 스키마에 정의됩니다.
2️⃣ 스키마의 종류.
1️⃣ 논리적 스키마(Logical Schema)
데이터베이스의 전체적인 논리적 구조를 나타냅니다.
테이블, 뷰, 인덱스 등의 논리적인 구조를 정의하며, 데이터베이스 사용자들이 데이터를 어떻게 볼지를 설명합니다.
2️⃣ 물리적 스키마(Physical Schema)
데이터가 실제로 데이터베이스에 어떻게 저장되는지를 나타냅니다.
이는 하드웨어 레벨에서 데이터가 저장되는 방식을 설명하며, 데이터 파일, 인덱스 파일 등이 포함됩니다.
3️⃣ 사용자 스키마(User Schema)
특정 사용자에게 보여지는 데이터베이스 구조를 나타냅니다.
일반적으로 사용자는 전체 데이터베이스가 아닌, 특정 뷰나 테이블만 접근할 수 있도록 설정됩니다.
3️⃣ 스키마 예시.
스키마는 SQL을 통해 정의할 수 있으며, 이를 통해 데이터베이스의 구조가 설정됩니다.
예를 들어, 사용자 테이블의 스키마는 다음과 같이 정의할 수 있습니다.
CREATE TABLE Users (
user_id INT PRIMARY KEY, -- 기본 키
username VARCHAR(50) NOT NULL -- NULL을 허용하지 않음
email VARCHAR(100) UNIQUE, -- 고유한 값
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
위 스키마 정의에서는 User 테이블이 생성되며, 다음과 같은 구조를 가집니다.
user_id: 기본 키(Primary Key)로, 고유한 사용자 식별자입니다.
username: 사용자의 이름을 저장하며, NULL을 허용하지 않습니다.
email: 사용자의 이메일을 저장하며, 고유한 값이어야 합니다.
created_at: 사용자가 생성된 시간을 나타내며, 기본값으로 현재 시간을 저장합니다.
4️⃣ 스키마의 역할.
데이터 구조 정의
스키마는 데이터베이스 내의 모든 데이터 구조를 정의합니다.
이는 테이블, 열(Column), 제약 조건 등을 명확하게 정의함으로써 데이터베이스의 일관성을 유지합니다.
데이터 무결성 유지
스키마는 제약 조건과 규칙을 설정하여 데이터의 무결성을 보장합니다.
예를 들어, 외래 키(Foreign Key) 제약 조건은 데이터 간의 참조 무결성을 유지합니다.
데이터베이스 관리
스키마 데이터베이스의 관리를 용이하게 하며, 개발자나 DBA(Database Administrator)가 데이터 구조를 설계하고 유지보수할 수 있도록 돕습니다.
5️⃣ 요약.
스키마(Schema)는 데이터베이스 구조의 설계를 나타내며, 데이터가 어떻게 저장되고 조직되는지를 정의합니다.
테이블(Table), 열(Column), 인덱스(Index), 뷰(View), 제약 조건(Constraints) 등이 스키마에 포함되며, 이를 통해 데이터베이스의 일관성과 무결성을 보장합니다.
스키마(Schema)는 데이터베이스의 논리적 구조를 정의하는 데 매우 중요한 요소이며, 데이터베이스의 효율적인 운영과 관리에 필수적입니다.
-
💾[Database] 데이터베이스 URL이란 무엇일까요?
💾[Database] 데이터베이스 URL이란 무엇일까요?
데이터베이스 URL은 애플리케이션이 데이터베이스에 연결할 때 사용되는 고유한 주소입니다.
URL은 애플리케시연이 데이터베이스 서버를 찾고, 연결할 데이터베이스를 지정하기 위한 정보를 포함하고 있습니다.
이 URL은 데이터베이스 종류에 따라 고유한 형식으로 작성되며, 보통 프로토콜, 서버 위치(호스트), 포트 번호, 데이터베이스 이름 등을 포함합니다.
1️⃣ 데이터베이스 URL의 기본 형식.
데이터베이스 URL은 보통 다음과 같은 형식을 따릅니다.
jdbc:<데이터베이스 유형>://<호스트>:<포트>/<데이터베이스 이름>?<옵션들>
각 부분의 의미는 다음과 같습니다.
jdbc
Java Database Connectivity의 약자로, JDBC URL임을 나타냅니다.
JDBC(Java Database Connectivity)를 사용하여 데이터베이스와 연결하는 표준 방식입니다.
<데이터베이스 유형>
사용하는 데이터베이스의 유형을 나타냅니다.
예를 들어, MySQL, PostgreSQL, Oracle, SQL Server 등이 여기에 들어갑니다.
<호스트>
데이터베이스가 실행되고 있는 서버의 주소를 나타냅니다.
로컬에서 실행될 경우 localhost를 사용하고, 원격 서버의 경우 서버 IP 주소나 도메인 이름을 사용합니다.
<포트>
데이터베이스 서버가 수신 대기 중인 네트워크 포트 번호입니다.
각 데이터베이스는 기본 포트가 있지만, 필요에 따라 다른 포트를 사용할 수도 있습니다.
MySQL: 3306
PostgreSQL: 5432
Oracle: 1521
<데이터베이스 이름>
연결할 데이터베이스의 이름입니다.
같은 데이터베이스 서버에서 여러 데이터베이스를 운영할 수 있으므로, 연결할 데이터베이스를 명시합니다.
<옵션들>
추가적으로 데이터베이스 연결을 위한 옵션을 전달할 수 있습니다.
예를 들어, 인코딩 방식이나 SSL 사용 여부 등의 설정이 여기에 포함될 수 있습니다.
📝 SSL(Secure Socket Layer)
네트워크 상에서 데이터를 안전하게 암호화하여 전송하기 위한 보안 프로토콜입니다.
SSL은 클라이언트와 서버 간의 통신을 암호화하여, 데이터가 전송되는 동안 제3자가 이를 도청하거나 위조하지 못하도록 보호합니다.
SSL은 특히 웹 브라우저와 웹 서버 간의 안전한 통신을 보장하는 데 널리 사용되었습니다.
현재는 SSL의 후속 버전인 TLS(Transport Layer Security)가 SSL을 대체하여 사용되고 있지만, 보통 사람들은 여전히 SSL이라는 용어를 사용해 TLS도 함께 지칭합니다.
2️⃣ 데이터베이스 URL 예시.
1️⃣ MySQL
jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC
mysql : MySQL 데이터베이스 사용
localhost : 데이터베이스 서버의 주소 (로컬에서 실행 중인 경우)
3306 : MySQL의 기본 포트 번호
mydatabase : 연결할 데이터베이스 이름
useSSL=false : SSL 사용 여부 (이 예시에서는 사용하지 않음)
serverTimezone=UTC : 서버의 타임존 설정
2️⃣ PostgreSQL
jdbc:postgresql://localhost:5432/mydatabase
postgresql : PostgreSQL 데이터베이스 사용
localhost : 데이터베이스 서버 주소
5432 : PostgreSQL의 기본 포트 번호
mydatabase : 연결할 데이터베이스 이름
3️⃣ Oracle
jdbc:oracle:thin:@localhost:1521:ORCL
oracle:thin : Oracle의 JDBC 드라이버 사용
localhost : 데이터베이스 서버 주소
1521 : Oracle의 기본 포트 번호
ORCL : 데이터베이스 서비스 이름
4️⃣ SQL Server
jdbc:sqlserver://localhost:1433;databaseName=mydatabase;integratedSecurity=true;
sqlserver : Microsoft SQL Server 사용
localhost : 데이터베이스 서버 주소
1433 : SQL Sever의 기본 포트 번호
mydatabase : 연결할 데이터베이스 이름
integratedSecurity=true : 통합 보안(Windows 인증) 사용
5️⃣ H2(In-Memory Database)
jdbc:h2:mem:testdb
h2:mem : H2 데이터베이스의 메모리 모드 사용
testdb : 메모리 내에서 사용할 데이터베이스 이름
3️⃣ 데이터베이스 URL 사용 방법.
데이터베이스 URL은 주로 Spring Boot와 같은 프레임워크나 JDBC(Java Database Connectivity) API를 통해 데이터베이스에 연결할 때 설정합니다.
예를 들어, Spring Boot에서 application.properties나 application.yml 파일에 데이터베이스 URL을 지정할 수 있습니다.
👉 application.properties에서 데이터베이스 URL 설정.
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=secret
👉 JDBC API로 데이터베이스 연결 예시
String url = "jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "secret";
Connection conn = DriverManager.getConnection(url, username, password);
4️⃣ 데이터베이스 URL의 구성 요소 요약.
프로토콜(JDBC) : jdbc:로 시작하며 JDBC(Java Database Connectivity)를 사용한 데이터베이스 연결을 의미합니다.
데이터베이스 유형 : 사용할 데이터베이스의 종류(MySQL, PostgreSQL, Oracle 등)를 지정합니다.
서버 주소(호스트와 포트) : 데이터베이스 서버의 위치를 나타냅니다(로컬 또는 원격).
데이터베이스 이름 : 연결하려는 데이터베이스의 이름을 지정합니다.
추가 옵션 : SSL 사용, 타임존 설정 등 추가적인 설정이 필요할 때 URL에 포함될 수 있습니다.
5️⃣ 요약.
데이터베이스 URL은 애플리케이션이 데이터베이스에 연결할 때 사용하는 고유한 주소로, 데이터베이스 유형, 서버 주소, 포트 번호, 데이터베이스 이름 등의 정보를 포함하여 연결 설정을 정의합니다.
-
🍃[Spring] JPA 어노테이션 - `@GeneratedValue`
🍃[Spring] JPA 어노테이션 - @GeneratedValue
@GeneratedValue는 JPA(Java Persistence API)에서 기본 키(Primary Key) 값을 자동으로 생성할 때 사용하는 어노테이션입니다.
이 어노테이션은 엔티티(Entity)의 기본 키(Primary Key) 필드에 값을 자동으로 할당하는 방식을 정의하며, 데이터베이스에서 기본 키(Primary Key)가 생성되는 방법을 설정할 수 있습니다.
1️⃣ 주요 특징.
1️⃣ 자동으로 기본 키 값 생성.
@GeneratedValue는 기본 키(Primary Key)에 수동으로 값을 할당하지 않고, 데이터베이스나 JPA(Java Persistence API) 구현체가 기본 키(Primary Key) 값을 자동으로 생성하도록 합니다.
2️⃣ 전략(GenerationType) 설정.
@GenerationType는 strategy 속성을 사용하여, 기본 키 값(Primary Key)을 생성하는 방식을 설정할 수 있습니다.
JPA(Java Persistence API)는 네 가지 생성 전략을 제공합니다.
AUTO
IDENTITY
SEQUENCE
TABLE
2️⃣ 생성 전략(GenerationType)
1️⃣ GenerationType.AUTO
기본 키(Primary Key) 생성 전략을 JPA(Java Persistence API) 구현체(예: Hibernate)가 자동으로 선택하도록 합니다.
데이터베이스에 맞는 최적의 방법을 JPA(Java Persistence API)가 결정합니다.
일부 데이터베이스에서는 SEQUENCE 방식, 다른 데이터베이스에서는 IDENTITY 방식 등을 사용할 수 있습니다.
2️⃣ GenerationType.IDENTITY
기본 키 값이 자동 증가하는 컬럼(Column,열)을 사용하는 방식입니다.
데이터베이스가 직접 기본 키(Primary Key) 값을 생성합니다.
예를 들어, MySQL에서는 AUTO_INCREMENT, SQL Server에서는 IDENTITY 컬럼(Column, 열)을 사용하여 값을 자동으로 증가시킵니다.
IDENTITY 전략은 데이터베이스에 의존적이며, 즉각적으로 값이 생성됩니다(데이터가 삽입되기 전에 미리 알 수 없음).
3️⃣ GenerationType.SEQUENCE
시퀀스 객체를 사용하여 기본 키 값을 생성합니다.
데이터베이스의 테이블을 통한 고유한 ID 값을 관리하며, 이 방식은 데이터베이스 독립적인 방식이지만 성능이 떨어질 수 있습니다.
3️⃣ @GeneratedValue 사용 예시.
1️⃣ AUTO 전략.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // 기본 키 생성 전략을 JPA가 자동으로 선택.
private Long id;
private String name;
private String email;
// 기본 생성자 및 Getter, Setter
public User() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
2️⃣ IDENTITY 전략.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 데이터베이스가 자동 증가하는 방식으로 기본 키(Primary Key) 생성
private Long productId;
private String name;
private Double price;
// 기본 생성자 및 Getter, Setter
public Product() {}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
3️⃣ SEQUENCE 전략.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.SequenceGenerator;
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq") // 시퀀스
@SequenceGenerator(name = "order_seq", sequenceName = "order_sequence", allocationSize = 1)
private Long orderId;
private String product;
private int quantity;
// 기본 생성자 및 Getter, Setter
public Order() {}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
위 코드에서는 @SequenceGenerator를 사용하여 시퀀스에 대한 세부 설정을 지정합니다.
sequenceName은 데이터베이스에서 사용할 시퀀스의 이름을 정의하고, allocationSize는 시퀀스 값을 미리 할당하는 크기를 지정합니다.
4️⃣ 요약.
@GeneratedValue는 JPA(Java Persistence API)에서 기본 키(Primary Key)를 자동으로 생성할 때 사용하는 어노테이션입니다.
생성 전략(GenerationType)에는 AUTO, IDENTITY, SEQUENCE, TABLE이 있으며, 각 전략은 기본 키(Primary Key)를 생성하는 방식에 따라 다르게 동작합니다.
AUTO는 JPA(Java Persistence API)가 자동으로 전략을 선택하고, IDENTITY는 데이터베이스의 자동 증가 기능을 사용하며, SEQUENCE는 시퀀스 객체를 통해 기본 키(Primary Key)를 생성하고, TABLE은 별도의 테이블을 통해 고유 값을 관리합니다.
이를 통해 JPA는 기본 키 값을 쉽게 생성하고 관리할 수 있어, 개발자는 기본 키 생성에 대해 신경 쓰지 않아도 됩니다.
-
🍃[Spring] JPA 어노테이션 - `@Column`
🍃[Spring] JPA 어노테이션 - @Column
@Column은 JPA(Java Persistence API)에서 사용되는 어노테이션으로, 엔티티(Entity) 클래스의 필드를 데이터베이스의 테이블 컬럼(Column, 열)에 매핑할 때 사용됩니다.
즉, 자바 클래스의 필드와 데이터베이스 테이블의 특정 컬럼(Column, 열) 간의 매핑을 정의하는 역할을 합니다.
🙋♂️엔티티(Entity)는 무엇일까요?
1️⃣ 주요 특징.
1️⃣ 컬럼(Column, 열)과 필드 매핑.
@Column 어노테이션은 엔티티(Entity) 클래스의 필드가 데이터베이스 테이블의 어느 컬럼(Column, 열)과 매핑될지를 명시적으로 지정합니다.
매핑되는 컬럼(Column)의 이름, 길이 nullable 여부, 고유 제약조건 등을 설정할 수 있습니다.
2️⃣ 옵션 설정.
@Column을 사용하여 컬럼(Column)의 속성(길이, nullable 여부 등) 세밀하게 제어할 수 있습니다.
만약 @Column을 생략하면 JPA(Java Persistence API)가 자동으로 필드 이름을 컬럼(Column, 열) 이름으로 사용하고, 기본값으로 처리합니다.
2️⃣ 주요 속성.
👉 name
매핑할 데이터베이스 컬럼(Column, 열)의 이름을 명시적으로 지정합니다.
지정하지 않으면, 필드 이름이 그대로 컬럼(Column, 열) 이름으로 사용됩니다.
👉 nullable
컬럼(Column, 열)이 NULL 값을 허용하는지 여부를 설정합니다.
기본값은 true로, nullable = false로 설정하면 NOT NULL 제약조건이 걸립니다.
👉 unique
컬럼(Column, 열)에 고유 제약조건을 설정할 수 있습니다.
기본값은 false이며, unique = true로 설정하면 해당 컬럼(Column, 열)에 고유한 값만 저장될 수 있습니다.
👉 length
문자열 컬럼(Column, 열)의 최대 길이를 설정합니다.
기본값은 255입니다.
주로 VARCHAR 타입 컬럼(Column, 열)에서 사용됩니다.
👉 precision
소수점이 포함된 숫자(예: BigDecimal)의 정밀도를 지정합니다.
precision은 소수점 앞의 전체 자릿수를 의미합니다.
👉 scale
소수점 이하 자릿수를 설정합니다.
scale은 소수점 이하의 자릿수를 의미합니다.
3️⃣ 예시.
1️⃣ 기본적인 사용.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Column;
@Entity
public class User {
@Id
private Long id;
// 매핑할 데이터베이스 컬럼의 이름을 "user_name"으로 지정.
// NOT NULL 제약 조건을 걸음.
// 문자열 컬럼의 최대 길이를 100으로 설정.
@Column(name = "user_name", nullable = false, length = 100)
private String name;
// 고유 제약조건을 설정.
// 고유한 값만 저장될 수 있음.
@Column(unique = true)
private String email;
// 기본 생성자, getter, setter
public User() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Column(name = "user_name", nullable = false, length = 100)
이 어노테이션은 name 필드를 데이터베이스의 user_name 컬럼에 매핑합니다.
이 컬럼은 NOT NULL 제약조건이 적용되며, 최대 길이는 100자로 제한됩니다.
@Column(unique = true)
이 어노테이션은 email 필드에 고유 제약조건을 설정하여, email 값이 유일하도록 보장합니다.
2️⃣ precision과 scale 사용 예시(소수점 처리)
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Column;
import javax.persistence.BigDecimal;
@Entity
public class Product {
@Id
private Long id;
@Column(precision = 10, scale = 2)
private BigDecimal price;
// 기본 생성자, getter, setter
public Product() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
@Column(precision = 10, scale = 2)
price 필드는 소수점 이하 두 자리를 포함하는 최대 10자리의 숫자로 저장됩니다.
예를 들어, 가격이 12345678.99와 같은 값을 가질 수 있습니다.
여기에서 precision은 숫자의 전체 자릿수를, scale은 소수점 이하 자릿수를 의미합니다.
4️⃣ @Column의 기본값.
@Column 어노테이션을 생략해도 JPA(Java Persistence API)는 자동으로 이름을 컬럼(Column, 열) 이름으로 사용하고, 기본 속성을 적용합니다.
예를 들어, 아래 코드에서 name 필드는 자동으로 name이라는 컬럼(Column, 열)과 매핑됩니다.
@Entity
public class User {
@Id
private Long id;
private String name; // @Column을 생략했지만 필드 이름이 컬럼 이름으로 사용됨
// 기본 생성자, getter, setter
}
5️⃣ 요약.
@Column 어노테이션은 엔티티 필드와 데이터베이스 컬럼(Column, 열) 간의 매핑을 정의하는 역할을 합니다.
이 어노테이션을 사용하면 컬럼(Column, 열)의 이름, nullable 여부, 고유 제약조건 등을 세밀하게 설정할 수 있습니다.
필수는 아니지만, 명시적으로 데이터베이스 컬럼(Column, 열)의 속성을 지정해야 할 경우 유용하게 사용할 수 있습니다.
이를 통해 개발자는 데이터베이스 테이블 구조에 맞추어 엔티티 클래스를 설계하고, 데이터베이스 컬럼(Column, 열)과의 매핑을 정확하게 설정할 수 있습니다.
-
☕️[Java] 클래스, 객체, 인스턴스
☕️[Java] 클래스, 객체, 인스턴스.
클래스(Class), 객체(Object), 인스턴스(Instance)는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서 중요한 개념이며, 서로 밀접하게 관련이 있지만 다른 의미를 가집니다.
🙋♂️ 객체 지향 프로그래밍(Object-Oriented Programming, OOP)
1️⃣ 클래스(Class)
클래스(Class)는 “객체(Object)를 정의하기 위한 청사진(설계도)”입니다.
클래스(Class)는 객체(Object)의 속성(필드)과 행동(메서드)을 정의합니다.
즉, 클래스는 어떤 유형의 객체(Object)를 만들 것인지에 대한 정보를 가지고 있으며, 객체(Object)를 생성하는 데 사용되는 틀 역할을 합니다.
예시.
// 클래스 선언.
public class Car {
// 필드(속성)
String model;
String color;
// 메서드(행동)
void drive() {
System.out.println("The car is driving.");
}
}
위의 Car 클래스는 자동차의 모델과 색상을 나타내는 속성(필드)과, 자동차가 달리는 행동을 정의하는 메서드를 가지고 있습니다.
이 자체는 실제 객체(Object)가 아니라 객체(Object)를 만들기 위한 설계도(blueprint, 청사진)입니다.
2️⃣ 객체(Object)
객체(Object)는 클래스(Class)에 의해 생성된 실체로, 클래스의 인스턴스(Instance)라고도 불립니다.
객체(Object)는 클래스(Class)에서 정의된 속성(필드)과 행동(메서드)를 실제로 가지고 있는 메모리 상의 실체입니다.
즉, 클래스의 설계도(blueprint, 청사진)을 바탕으로 만들어진 실질적인 데이터입니다.
객체(Object)는 클래스(Class)에서 정의된 구조에 따라 동작하며, 프로그램 내에서 데이터와 기능을 담당합니다.
객체(Object) 생성 예시.
public class Main {
public static void main(String[] args) {
// 객체 생성
Car myCar = new Car(); // Car 클래스의 객체 생성.
myCar.model = "Volvo xc60";
myCar.color = "Black";
myCar.drive(); // 출력: The car is driving
}
}
위 코드에서 Car myCar = new Car();는 Car 클래스(Class)의 객체(Object)를 생성합니다.
이 myCar 객체(Object)는 Car 클래스(Class)에서 정의한 속성(필드)과 행동(메서드)를 가지고 있으며, 프로그램에서 실제로 사용됩니다.
3️⃣ 인스턴스(Instance)
인스턴스(Instance)는 특정 클래스(Class)에서 생성된 객체(Object)를 의미합니다.
즉, 클래스(Class)의 인스턴스(Instance)는 그 클래스(Class)에 의해 생성된 실체 객체(Object)를 말합니다.
흔히 인스턴스(Instance)와 객체(Object)는 같은 의미로 사용되지만, “인스턴스(Instance)”는 특정 클래스(Class)에서 만들어진 객체(Object)라는 의미에 좀 더 초점을 맞추고 있습니다.
예를 들어, myCar는 Car 클래스의 인스턴스이며, 또한 객체입니다.
인스턴스(Instance) 예시.
Car myCar = new Car(); // Car 클래스의 인스턴스 생성.
Car yourCar = new Car(); // 또 다른 Car 클래스의 인스턴스 생성.
위 코드에서 myCar와 yourCar는 모두 Car 클래스의 인스턴스(Instance)입니다.
즉, 동일한 클래스(Class)에서 만들어진 여러개의 객체(인스턴스)를 생성할 수 있습니다.
4️⃣ 요약.
클래스(Class) : 객체(Object)를 생성하기 위한 설계도(blueprint, 청사진)입니다. 속성(필드)과 행동(메서드)를 정의합니다.
객체(Object) : 클래스(Class)에 의해 생성된 실체입니다. 프로그램 내에서 사용되며, 클래스의 인스턴스(Instance)라고도 불립니다.
인스턴스(Instance) : 특정 클래스에서 생성된 객체(Object)를 가리키는 용어입니다. 클래스의 실체로서 메모리 내에 존재하는 객체(Object)를 의미합니다.
객체(Object)와 인스턴스(Instance)는 서로 거의 같은 의미로 사용되며, 클래스(Class)는 이 객체(Object)들을 만들기 위한 설계도(blueprint, 청사진)입니다.
-
🍃[Spring] JPA 어노테이션 - `@Entity`
🍃[Spring] JPA 어노테이션 - @Entity
@Entity는 JPA(Java Persistence API)에서 데아터베이스 테이블과 매핑되는 자바 클래스를 정의할 때 사용하는 어노테이션(Annotation)입니다.
이 어노테이션(Annotation)을 클래스에 붙이면 해당 클래스가 JPA(Java Persistence API) 엔티티(Entity)임을 나타내며, JPA가 이를 데이터베이스 테이블과 매핑하여 관리할 수 있게 됩니다.
📝 엔티티(Entity)
객체 지향 프로그래밍(Object-Oriented Programming, OOP)과 데이터베이스(Database) 설계에서 모두 사용되는 개념으로, 특히 JPA(Java Persistence API)에서 자주 언급됩니다.
엔티티(Entity)는 데이터베이스 테이블과 매핑되는 자바 클래스를 의미하며, 데이터베이스의 각 행(Row)이 자바 클래스의 객체(Instance, 인스턴스)로 대응됩니다.
1️⃣ 주요 특징.
1️⃣ 엔티티 클래스(Entity Class)
@Entity가 선언된 클래스는 엔티티(Entity)라고 하며, 이는 데이터베이스 테이블에 대응하는 자바 클래스입니다.
엔티티 클래스의 인스턴스(Instance, 객체)는 데이터베이스 테이블의 각 행(Row)에 대응됩니다.
클래스는 반드시 기본 생성자(default constructor)를 가져야 하고, 기본 키(Primary Key)를 정의해야 합니다.
2️⃣ 테이블 매핑(Table Mapping)
클래스에 @Entity 어노테이션(Annotation)을 붙이면, JPA(Java Persistence API)는 해당 클래스를 데이터베이스의 테이블과 매핑합니다.
테이블의 이름은 기본적으로 클래스 이름과 동일하게 매핑되지만, @Table 어노테이션(Annotation)을 사용하여 테이블 이름을 명시적으로 지정할 수도 있습니다.
3️⃣ 기본 키(Primary Key)
엔티티 클래스는 반드시 하나의 필드를 기본 키(Primary Key)로 지정해야 하며, 이를 위해 @Id 어노테이션(Annotation)을 사용합니다.
기본 키(Primary Key)의 생성 전략은 @GeneratedValue 어노테이션(Annotation)을 통해 지정할 수 있습니다.
2️⃣ 사용 예시.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Table;
@Entity // 이 클래스는 JPA 엔티티임을 나타냄.
@Table(name = "users") // 매핑될 데이터베이스 테이블 이름을 지정.
public class User {
@Id
@GeneratedValue(strategy = GenerarionType.IDENTITY) // 기본 키 생성 전략
private Long id;
private String name;
private String email;
// 기본 생성자 (필수)
public User() {}
// 생성자, getter, setter 등
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getter and Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
3️⃣ 주요 요소.
1️⃣ @Entity
이 어노테이션은 JPA(Java Persistence API)에게 해당 클래스가 데이터베이스의 테이블과 매핑되는 엔티티(Entity)임을 알립니다.
2️⃣ @Table
테이블 이름을 명시적으로 지정하려면 @Table(name = "테이블이름")을 사용합니다.
@Table을 사용하지 않으면 기본적으로 클래스 이름이 테이블 이름으로 사용됩니다.
3️⃣ @Id
해당 필드는 엔티티(Entity)의 기본 키(Primary Key)로 사용됩니다.
모든 엔티티는 반드시 하나의 기본 키(Primary Key)를 가져야 합니다.
4️⃣ @GeneratedValue
기본 키(Primary Key) 값의 생성 전략을 지정합니다.
예를 즐어, GenerationType.IDENTITY는 데이터베이스가 자동으로 기본 키(Primary Key) 값을 증가시키는 전략을 의미합니다.
4️⃣ 주의 사항.
엔티티 클래스(Entity Class)는 반드시 기본 생성자(default constructor)가 있어야 하며, public 또는 protected 접근 제어자를 가져야 합니다.
기본 키(Primary Key)를 @Id 어노테이션으로 반드시 지정해야 합니다.
5️⃣ 요약.
@Entity는 JPA(Java Persistence API) 엔티티를 선언하는 어노테이션으로, 해당 클래스를 데이터베이스 테이블과 매핑합니다.
이를 통해 JPA는 엔티티를 자동으로 관리하고, 데이터베이스와 상호작용할 수 있게 됩니다.
-
🍃[Spring] JPA 어노테이션 - `@Id`
🍃[Spring] JPA 어노테이션 - @Id
JPA 어노테이션 중 @Id는 엔티티의 기본 키(Primary Key)를 지정하는 데 사용되는 어노테이션(Annotation)입니다.
기본 키(Primary Key)는 데이터베이스 테이블에서 각 행(Row)을 고유하게 식별하는 데 사용되며, 모든 JPA(Java Persistence API) 엔티티는 반드시 하나의 필드를 기본 키(Primary Key)로 지정해야 합니다.
1️⃣ 주요 특징.
1️⃣ 기본 키(Primary Key) 지정.
@Id 어노테이션을 통해 엔티티의 필드나 속성을 기본 키(Primary Key)로 지정합니다.
기본 키(Primary Key)는 데이터베이스 테이블에서 각 행(Row)을 고유하게 식별하는 값으로, 각 엔티티 인스턴스를 고유하게 식별하는 데 사용됩니다.
2️⃣ 단일 또는 복합 키.
@Id는 단일 필드를 기본 키(Primary Key)로 지정할 때 사용되며, 복합 키(두 개 이상의 필드로 구성된 기본 키(Primary Key))를 지정할 때는 @IdClass 또는 @EmbeddedId 어노테이션을 함께 사용할 수 있습니다.
3️⃣ 자동 생성.
기본 키(Primary Key)는 자동으로 생성될 수 있으며, 이를 위해 @GeneratedValue 어노테이션과 함께 사용합니다.
@GeneratedValue는 기본 키(Primary Key) 생성 전략을 정의합니다.(예: AUTO, IDENTITY, SEQUENCE, TABLE).
2️⃣ 예시.
1️⃣ 단순한 기본 키(Primary Key) 지정.
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity // 엔티티 클래스 선언
public class User {
@Id // 기본 키로 지정
private Long id; // 이 필드가 기본 키가 됨
private String name;
private String email;
// 기본 생성자 및 Getter, Setter
public User() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
위 예시에서 id 필드는 @Id 어노테이션으로 지정되어 기본 키(Primary Key)가 됩니다.
기본 키(Primary Key)는 고유하게 엔티티를 식별하는 값입니다.
2️⃣ 기본 키 자동 생성.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class Product {
@Id // 기본 키(Primary Key)로 지정.
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키(Primary Key) 자동 생성/
private Long productId; // 이 필드가 자동 생성되는 기본 키.
private String name;
private Double price;
// 기본 생성자 및 Getter and Setter
public Product () {}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
이 예시에서는 @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하여 productId가 데이터베이스에서 자동으로 생성됩니다.
IDENTITY 전략은 데이터베이스가 자동으로 증가하는 값을 기본 키(Primary Key)로 사용하도록 합니다.
3️⃣ 기본 키(Primary Key) 생성 전략.
@GeneratedValue 어노테이션과 함께 사용하여 기본 키(Primary Key)가 자동으로 생성되도록 설정할 수 있습니다.
생성 전략은 네 가지가 있습니다.
1️⃣ AUTO
JPA 구현체가 적절한 생성 전략을 자동으로 선택합니다.
2️⃣ IDENTITY
기본 키(Primary Key) 값을 데이터베이스가 자동으로 증가시켜 생성합니다(주로 MySQL에서 사용)
3️⃣ SEQUENCE
데이터베이스 시퀀스를 사용하여 기본 키(Primary Key) 값을 생성합니다.(주로 Oracle에서 사용).
📝 데이터베이스 시퀀스(Sequence)
데이터베이스에서 고유한 숫자 값을 순차적으로 생성하기 위해 사용되는 객체입니다.
주로 기본 키(Primary Key)나 다른 고유 식별자를 자동으로 생성하는 용도로 사용됩니다.
시퀀스(Sequence)는 특정 규칙에 따라 숫자를 생성하며, 자동으로 증가하는 숫자 값을 제공해 데이터베이스의 여러 행(Row)에 고유한 값을 할당할 수 있습니다.
4️⃣ TABLE
키 값을 저장하기 위한 별도의 테이블을 사용합니다.
4️⃣ 요약.
@Id는 JPA에서 엔티티의 기본 키(Primary Key)를 지정하는 어노테이션으로, 엔티티의 각 인스턴스를 고유하게 식별합니다.
기본 키(Primary Key)는 필수적으로 정의해야 하며, 필요에 따라 @GeneratedValue를 사용해 자동으로 생성할 수 있습니다.
-
🍃[Spring] Hibernate와 JDBC는 어떤 관계인가요?
🍃[Spring] Hibernate와 JDBC는 어떤 관계인가요?
Hibernate와 JDBC(Java Database Connectivity)는 모두 자바에서 데이터베이스와 상호작용하는 방식이지만, 서로 다른 수준에서 작동하는 도구입니다.
Hibernate는 JDBC(Java Database Connectivity)를 내부적으로 사용하여 데이터베이스와의 연결을 관리하고 쿼리를 실행하지만, 그 역할과 목적이 다릅니다.
이 둘의 관계와 차이점을 이해하기 위해 각 도구를 살펴보겠습니다.
1️⃣ JDBC(Java Database Connectivity)
JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 직접 연결하고 SQL 쿼리를 실행할 수 있게 해주는 저수준 API입니다.
JDBC(Java Database Connectivity)는 개발자가 데이터베이스에 SQL(Structured Query Language) 문을 작성하고, 결과를 처리하며, 데이터베이스와 직접 상호작용할 수 있도록 해줍니다.
JDBC(Java Database Connectivity)는 모든 SQL(Structured Query Language) 작업(삽입, 갱신, 삭제, 조회)을 수동으로 처리해야 하며, 데이터베이스 연결 관리, 쿼리 실행, 결과 집합(ResultSet) 처리, 예외 처리 등을 개발자가 직접 관리해야 합니다.
👉 JDBC 예시.
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM Users WHERE id = ?");
pstmt.setInt(1, 1);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
위의 코드에서 개발자는 SQL을 직접 작성하고, Connection 및 PreparedStatement와 같은 JDBC(Java Database Connectivity) 객체를 이용해 데이터베이스 작업을 처리해야 합니다.
2️⃣ Hibernate
Hibernate는 자바에서 ORM(Object-Relational Mapping) 프레임워크로, 데이터베이스와의 상호작용을 더 추상화된 고수준 방식으로 처리합니다.
Hibernate는 데이터베이스 테이블과 자바 객체 간의 매핑을 통해, 직접적인 SQL(Structured Query Language) 쿼리 작성 없이도 데이터베이스 작업을 수행할 수 있습니다.
Hibernate는 내부적으로 JDBC(Java Database Connectivity)를 사용하여 데이터베이스와 연결하지만, 개발자는 이를 직접 다룰 필요가 없습니다.
대신, 객체 중심적인 API를 사용하여 데이터베이스 작업을 처리할 수 있습니다.
Hibernate는 데이터베이스 연결 관리, 캐싱, 트랜 잭션 관리 쿼리 생성을 모두 자동화하거나 쉽게 처리할 수 있게 해줍니다.
👉 Hibernate 예시.
Session session = sessionFactory.openSession();
User user = session.get(User.class, 1); // SQL 작성 없이 객체로 데이터 조회
System.out.println(user.getName());
위의 예시에서는 SQL(Structured Query Language)을 작성할 필요 없이 Hibernate가 SQL(Structured Query Language)를 자동으로 생성하고 실행하여 객체를 반환합니다.
3️⃣ Hibernate와 JDBC의 관계 및 차이점.
1️⃣ 추상화(Abstraction) 수준.
JDBC(Java Database Connectivity)는 데이터베이스와의 상호작용을 처리하는 저수준 API입니다.
개발자는 SQL(Structure Query Language)을 직접 작성하고 실행해야 하며, 연결, 트랜잭션, 예외 처리 등도 관리해야 합니다.
Hibernate는 고수준 ORM(Object-Relational Mapping) 프레임워크로, JDBC(Java Database Connectivity) 내부적으로 사용하여 SQL(Structured Query Language) 쿼리를 실행하고 데이터베이스와 상호작용하지만, 개발자에게는 객체 지향적인 API를 제공합니다.
따라서 SQL(Structured Query Language) 대신 자바 객체를 통해 데이터베이스와 상호작용할 수 있습니다.
2️⃣ SQL 작성.
JDBC(Java Database Connectivity)는 SQL(Structured Query Language)을 직접 작성해야 하며, 데이터베이스의 테이블 구조를 이해하고 그에 맞는 쿼리를 작성해야 합니다.
Hibernate는 SQL(Structured Query Language)을 자동으로 생성하거나, HQL(Hibernate Query Language)과 같은 객체 지향적인 쿼리 언어를 사용할 수 있어 SQL(Structured Query Language)을 직접 작성하지 않고도 데이터를 처리할 수 있습니다.
3️⃣ 데이터베이스 독립성.
JDBC(Java Database Connectivity)는 특정 데이터베이스에 맞춰 SQL을 작성해야 하므로 데이터베이스 종속적인 코드가 될 수 있습니다.
Hibernate는 특정 데이터베이스에 종속되지 않으며, 여러 데이터베이스 간의 전환이 쉽습니다.
SQL(Structured Query Language)을 자동으로 생성할 때 데이터베이스 종속 적인 차이를 처리해줍니다.
4️⃣ 트랜잭션 및 연결 관리.
JDBC(Java Database Connectivity)에서는 개발자가 직접 트랜잭션을 시작하고 종료해야 하며, 데이터베이스 연결도 수동으로 관리해야 합니다.
Hibernate는 트랜잭션과 연결 관리를 자동화하여, 개발자가 이러한 세부 사항을 신경 쓸 필요가 없습니다.
트랜잭션은 세션 단위로 처리되며, 데이터베이스 연결되 자동으로 처리됩니다.
5️⃣ 캐싱 및 성능 최적화.
JDBC(Java Database Connectivity)는 캐싱 기능이 없으므로, 데이터베이스 성능 최적화를 개발자가 수동으로 처리해야 합니다.
Hibernate는 1차 캐시와 2차 캐시를 제공하여, 반복적인 데이터베이스 접근을 최소화하고 성능을 최적화할 수 있습니다.
📝 1차 캐시와 2차 캐시.
1차 캐시와 2차 캐시는 Hibernate에서 성능을 최적화하기 위해 사용되는 캐싱 메커니즘입니다.
캐시는 데이터를 메모리에 저장하여 데이터베이스 접근을 줄이고, 애플리케이션의 성능을 향상시키는 데 중요한 역할을 합니다.
1️⃣ 1차 캐시(First-Level Cache)
세션 범위의 캐시로, Hibernate에서 기본적으로 제공되는 캐시입니다.
세션(Session) 동안만 유지되며, 각 세션마다 독립적으로 존재합니다.
즉, 동일한 세션에서 반복적으로 동일한 데이터를 조회할 때 데이터베이스에 다시 접근하지 않고 캐시된 데이터를 반환합니다.
1차 캐시는 자동으로 활성화되어 있으며, 개발자가 직접 설정할 필요가 없습니다.
1차 캐시 덕분에, 같은 세션 내에서 동일한 엔티티를 여러 번 조회해도 데이터베이스에 불필요한 접근을 줄일 수 있습니다.
2️⃣ 2차 캐시(Second-Level Cache)
세션 팩토리(SessionFactory) 범위의 캐시로, 여러 세션에 걸쳐 데이터를 공유할 수 있습니다.
선택적으로 활성화해야 하며, 기본적으로 활성화 되어 있지 않습니다.
개발자가 직접 설정을 통해 활성화할 수 있습니다.
2차 캐시는 여러 세션 간에 데이터를 재사용하여, 자주 조회되는 데이터를 메모리에 저장하고 데이터베이스 접근을 줄입니다.
즉, 동일한 데이터에 대해 세션을 종료한 후에도 2차 캐시에 저장된 데이터를 여러 세션에서 재사용할 수 있습니다.
2차 캐시는 다양한 캐시 제공자(예: EHCache, Infinispan)를 사용하여 구현할 수 있으며, Hibernate가 제공하는 설정을 통해 제어됩니다.
4️⃣ 결론.
Hibernate는 JDBC(Java Database Connectivity)를 내부적으로 사용하여 데이터베이스와 상호작용하지만, JDBC(Java Database Connectivity)보다 더 높은 추상화(Abstraction) 수준에서 ORM(Object-Relational Mapping) 기능을 제공하여, 개발자가 객체 지향적으로 데이터를 처리할 수 있게 해줍니다.
JDBC(Java Database Connectivity)는 SQL(Structured Query Language)을 직접 작성하고 데이터베이스와의 저수준 작업을 다루는 반면, Hibernate는 이러한 세부 사항을 추상화(Abstraction)하여 더 쉽게 데이터베이스와 상호작용할 수 있도록 도와줍니다.
Hibernate는 실질적으로 JDBC(Java Database Connectivity)의 기능을 기반으로 동작하지만, 더 높은 수준의 기능을 제공합니다.
-
💾 [CS] 엔티티(Entity)는 무엇일까요?
💾 [CS] 엔티티(Entity)는 무엇일까요?
엔티티(Entity)는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)과 데이터베이스 설계에서 모두 사용되는 개념으로, 특히 JPA(Java Persistence API)에서 자주 언급됩니다.
엔티티(Entity)는 데이터베이스 테이블과 매핑되는 자바 클래스를 의미하며, 데이터베이스의 각 행(Row)이 자바 클래스의 인스턴스(Instance, 객체)로 대응됩니다.
1️⃣ 엔티티의 정의.
1️⃣ 데이터베이스의 행(Record).
데이터베이스에서는 테이블이 여러 행(Record)을 가집니다.
엔티티(Entity)는 그 테이블의 각 행(Record)을 자바 객체(Intance)로 변환됩니다.
예를 들어, User 테이블이 있다면, 테이블의 각 레코드는 User 엔티티 객체로 변환됩니다.
2️⃣ JPA에서의 엔티티.
JPA(Java Persistence API)에서는 엔티티가 데이터베이스 테이블과 매핑되며, 이를 위해 클래스에 @Entity 어노테이션(Annotation)을 붙입니다.
엔티티(Entity) 클래스의 인스턴스(Instance, 객체)는 데이터베이스에서 하나의 행(Row, Record)을 나타내며, 그 필드는 테이블의 각 열(Column)에 매핑됩니다.
3️⃣ 객체 지향적 데이터 모델링.
엔티티(Entity)는 데이터베이스의 레코드(Record)를 단순히 자바 객체로 변환하는 것뿐만 아니라, 객체 지향 프로그래밍에(Object-Oriented Programming, OOP)에서의 상태(속성)와 행동(메서드)을 가질 수 있습니다.
즐, 데이터와 그 데이터를 처리하는 메서드가 함께 정의됩니다.
2️⃣ 엔티티의 특징.
👉 클래스와 데이터베이스 테이블 매핑.
엔티티 클래스는 보통 하나의 데이터베이스 테이블과 매핑됩니다.
👉 필드와 열(Column) 매핑.
엔티티 클래스의 필드는 테이블의 열(Column)과 매핑됩니다.
👉 기본 키(Primary Key).
엔티티는 반드시 하나의 필드를 기본 키(Primary Key)로 지정해야 합니다.
이 필드는 테이블에서 각 행(Row)을 고유하게 식별하는 데 사용됩니다.
👉 상태 관리.
엔티티는 JPA(Java Persistence API)가 관리하며, 엔티티의 상태(생성, 수정, 삭제)를 자동으로 데이터베이스에 반영할 수 있습니다.
3️⃣ 엔티티 클래스의 예.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity // 이 클래스는 데이터베이스 테이블과 매핑되는 엔티티임을 나타냅니다.
public class User {
@Id // 기본 키(Primary Key)를 지정.
@GeneratedValue(strategy = Generation.IDENTITY) // 기본 키(Primary Key) 자동 생성 전략 설정.
private Long id;
private String name;
private String email;
// 기본 생성자.
public User() {}
// 생성자, getter 및 setter
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getter and Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
4️⃣ 엔티티의 주요 요소.
1️⃣ @Entity
이 어노테이션은 클래스가 데이터베이스 테이블과 매핑된다는 것을 의미합니다.
2️⃣ @Id
엔티티 클래스의 필드 중 하나는 반드시 기본 키(Primary Key)로 지정되어야 하며, @Id 어노테이션을 사용합니다.
3️⃣ @GeneratedValue
기본 키(Primary Key)가 자동으로 생성되도록 설정할 수 있습니다.
GenerationType.IDENTITY는 데이터베이스가 자동으로 키를 증가시키도록 하는 전략입니다.
5️⃣ 엔티티의 장점.
👉 객체 지향 프로그래밍과 데이터베이스의 통합.
엔티티를 사용하면 데이터베이스의 테이블과 자바 객체를 일관된 방식으로 다룰 수 있어, 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
👉 자동화된 데이터베이스 작업.
JPA와 같은 프레임워크는 엔티티의 상태를 추적하여, 데이터베이스에서 발생하는 작업(삽입, 갱신, 삭제)을 자동으로 처리해줍니다.
👉 데이터베이스 독립성.
엔티티를 사용하면 특정 데이터베이스에 종속되지 않고 다양한 데이터베이스에서 동일한 코드를 사용할 수 있습니다.
6️⃣ 요약.
엔티티(Entity)는 데이터베이스 테이블과 매핑되는 자바 클래스이며, JPA(Java Persistence API)를 통해 객체 지향적인 방식으로 데이터베이스와 상호작용할 수 있게 해줍니다.
엔티티 클래스는 데이터베이스의 테이블을 자바 객체(Instance)로 표현하고, 테이블의 각 행(Row)을 엔티티 객체로 변환하여 데이터베이스 작업을 쉽게 처리할 수 있도록 도와줍니다.
-
🍃[Spring] Hibernate란 무엇일까요?
🍃[Spring] Hibernate란 무엇일까요?
Hibernate는 자바에서 사용되는 ORM(Object-Relational Mapping) 프레임워크로, 관계형 데이터베이스(Relational Database, RDB)와 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 간의 불일치를 해결해 주는 도구입니다.
객체 지향적인 자바 애플리케이션의 데이터를 관계형 데이터베이스(Relational Database, RDB)의 테이블에 자동으로 매핑해주는 역할을 합니다.
1️⃣ 주요 특징.
1️⃣ ORM(Object-Relational Mapping)
데이터베이스 테이블과 자바 클래스 간의 매핑을 통해, SQL(Structured Query Language) 문을 직접 작성하지 않고도 자바 객체로 데이터베이스 작업을 처리할 수 있습니다.
예를 들어, 데이터베이스의 테이블 행(Row)을 자바 객체로 변환하고, 자바 객체를 테이블의 행(Row)으로 변환하는 과정이 자동으로 이루어집니다.
2️⃣ HQL(Hibernate Query Language)
SQL(Structed Query Language)과 유사하지만, 데이터베이스 테이블이 아닌 자바 객체를 대상으로 질의를 할 수 있도록 설계된 쿼리 언어입니다.
데이터베이스에 종속되지 ㅇ낳아 다른 DBMS(Database Management System, 데이터베이스 관리 시스템)로의 전환이 용이합니다.
3️⃣ 캐싱(Caching)
Hibernate는 1차, 2차 캐싱을 제공하여 성능을 최적화합니다.
이를 통해 동일한 데이터를 여러 번 요청 할 때 데이터베이스에 불필요한 접근을 줄일 수 있습니다.
4️⃣ 트랜잭션 관리.
데이터베이스의 트랜잭션을 효과적으로 관리해주며, 여러 데이터베이스 작업을 하나의 단위로 처리할 수 있게 도와줍니다.
5️⃣ 자동 스키마 생성.
Hibernate는 자바 클래스를 기반으로 데이터베이스 스키마를 자동으로 생성하고 관리할 수 있습니다.
2️⃣ 장점.
👉 DB 독립성.
Hibernate는 특정 데이터베이스에 종속되지 않으며, 여러 데이터베이스에 쉽게 적용할 수 있습니다.
👉 생산성 향상.
SQL을 직접 작성할 필요가 없으므로 개발자의 생산성을 높일 수 있습니다.
👉 유지보수 용이.
객체지향적인 코드를 유지하며 데이터베이스 작업을 처리할 수 있어, 코드의 가독성과 유지 보수성이 높습니다.
3️⃣ 간단한 예.
@Entity
@Table(name = "User")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
}
위의 예시에서 User 클래스는 데이터베이스 테이블에 매핑됩니다.
@Entity와 @Table 애노테이션을 통해 클래스가 테이블과 매핑되고, @Id 애노테이션은 기본 키(Primary Key)를 나타냅니다.
Hibernate는 자바 애플리케이션이 데이터베이스와 상호작용할 때 객체 지향적으로 데이터를 처리할 수 있도록 도와주며, 데이터베이스의 복잡한 작업을 쉽게 관리할 수 있게 해줍니다.
-
🍃[Spring] JPA와 Hibernate는 어떤 관계인가요?
🍃[Spring] JPA와 Hibernate는 어떤 관계인가요?
JPA(Java Persistence API)와 Hibernate는 밀접한 관계가 있지만 서로 다른 개념입니다.
그들의 관계를 이해하기 위해서는 각각의 정의와 역할을 살펴볼 필요가 있습니다.
1️⃣ JPA(Java Persistence API).
JPA(Java Persistence API)는 자바 표준 명세(Java Specification)로, 자바 애플리케이션에서 관계형 데이터베이스(Relational Database, RDB)를 쉽게 사용할 수 있도록 ORM(Obeject-Relational Mapping)을 제공하는 인터페이스(Interface) 모음입니다.
JPA(Java Persistence API)는 자바 개발자에게 ORM(Object-Relational Mapping) 기능을 제공하기 위해 도입된 표준 API로, 특정 구현체가 아닙니다.
즉, JPA(Java Persistence API) 자체는 기능을 제공하지 않고, ORM(Object-Relational Mapping)의 표준 규격만 정의합니다.
JPA(Java Persistence API)를 사용하면 애플리케이션 코드가 특정 구현체에 종속되지 않도록 추상화(Abstraction)된 인터페이스(Interface)를 제공합니다.
2️⃣ Hibernate
Hibernate는 JPA(Java Persistence API) 표준을 구현한 ORM(Object-Relational Mapping) 프레임워크 중 하나입니다.
즉, JPA(Java Persistence API)가 정의한 규격을 여러 구현체 중 하나로, 가장 널리 사용되는 구현체입니다.
Hibernate는 JPA(Java Persistence API)를 구현하면서, 추가적인 기능도 제공하는 강력한 ORM(Object-Relational Mapping) 프레임워크입니다.
예를 들어, 캐싱, 스키마 자동 생성, HQL(Hibernate Query Language) 등의 고유한 기능을 포함합니다.
Hibernate는 JPA(Java Persistence API)의 표준 인터페이스(Interface)를 지원하면서도, 자체적인 API(Application Programming Interface)도 함께 제공하여 더 많은 기능을 사용할 수 있게 합니다.
3️⃣ 관계 요약.
JPA(Java Persistence API)는 ORM(Object-Relational Mapping)의 표준 인터페이스(Interface)로서, 구현체가 필요합니다.
Hibernate는 JPA(Java Persistence API) 표준을 구현한 구현체 중 하나입니다.
즉, Hibernate는 JPA(Java Persistence API)의 규격을 따르면서도 자체적인 확장 기능을 제공하는 ORM(Object-Relational Mapping) 프레임워크(Framework)입니다.
4️⃣ JPA와 Hibernate의 사용 방식.
1️⃣ JPA(Java Persistence API) 사용.
JPA(Java Persistence API)는 인터페이스(Interface)이기 때문에 애플리케이션 코드가 특정 ORM(Object-Relational Mapping) 구현체에 의존하지 않도록 합니다.
개발자는 JPA(Java Persistence API) 표준을 따르면서, Hibernate 같은 구현체를 선택해 사용할 수 있습니다.
👉 예시
@PersistenceContext
private EntityManager entityManager;
public void saveUser(User user) {
entityManager.persist(user); // JPA 표준 API 사용
}
2️⃣ Hibernate 사용
Hibernate는 JPA를 구현하면서 자체적인 API(Application Programming Interface)도 제공합니다.
개발자는 JPA(Java Persistence API) 표준 API(Application Programming Interface)를 사용하거나, Hibernate의 고유한 기능을 사용하기 위해 Hibernate의 API(Application Programming Interface)를 사용할 수 있습니다.
👉 예시
Session session = sessionFactory.openSession();
session.save(user); // Hibernate 고유 API 사용
5️⃣ 결론.
JPA(Java Persistence API)는 ORM(Object-Relational Mapping)을 위한 표준 API이고, Hibernate는 그 표준을 구현한 구체적인 구현체입니다.
JPA(Java Persistence API)는 독립적이지만, Hibernate는 JPA를 따르며 추가적인 기능을 제공합니다.
Hibernate를 사용하면 JPA(Java Persistence API) 표준 인터페이스로 개발하면서도 필요한 경우 Hibernate의 고유 기능을 사용할 수 있습니다.
-
💾 [CS] 객체 지향 프로그래밍(Object-Oriented Programming, OOP)는 무엇일까요?
💾 [CS] 객체 지향 프로그래밍(Object-Oriented Programming, OOP)는 무엇일까요?
객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 객체(Object)를 중심으로 구성하는 프로그래밍 패러다임입니다.
객체(Object)는 데이터와 이 데이터를 처리하는 함수를 묶은 개념으로, 현실 세계의 사물을 프로그램 내에서 모방하여 설계하는 방식입니다.
객체 지향 프로그래밍(Object-Oriented Programming, OOP)의 핵심 개념은 다음과 같습니다.
🙋♂️ 객체 지향 프로그래밍에서의 객체(Object)란 무엇일까요?
1️⃣ 클래스와 객체.
👉 클래스(Class).
객체(Object)를 생성하기 위한 청사진이나 틀입니다.
클래스는 속성(데이터)과 메서드(함수)를 정의합니다.
👉 객체(Object).
클래스(Class)를 기반으로 생성된 실체로, 클래스에 정의된 속성과 메서드를 사용합니다.
객체는 클래스의 인스턴스라고도 불립니다.
👉 예시.
// 클래스 정의
class Car {
// 속성 (필드)
String model;
String color;
// 생성자
public Car(String model, String color) {
this.model = model;
this.color = color;
}
// 메서드
public void drive() {
System.out.println(model + "이(가) 달립니다.");
}
}
// 객체 생성 및 사용
public class Main {
public static void main(String[] args) {
// Car 클래스의 인스턴스(객체) 생성.
Car car1 = new Car("Volvo xc60", "Black");
car1.drive(); // 출력: Volvo xc60이(가) 달립니다.
}
}
위 예시는 Car라는 클래스를 정의하고, 그 클래스를 기반으로 Volvo xc60이라는 객체를 생성한 후 drive() 메서드를 호출하는 과정입니다.
2️⃣ 캡슐화(Encapsulation)
객체 내부의 데이터(속성)와 이 데이터를 조작하는 메서드를 하나로 묶는 것을 말합니다.
캡슐화(Encapsulation)를 통해 객체(Object) 외부에서는 내부 구현을 알지 못하게 하고, 제공된 메서드를 통해서만 데이터를 접근하거나 변경할 수 있게 만듭니다.
이를 통해 데이터 보호와 코드의 응집성을 높일 수 있습니다.
👉 예시
class Person {
// private 속성 (외부에서 직접 접근할 수 없음)
private String name;
private int age;
// 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// public 메서드를 통해 간접적으로 접근
public String getName() {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if(age > 0) { // 유효성 검사
this.age = age;
}
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person("Kobe", 25);
System.out.println(p.getName()); // 출력: Kobe
p.setAge(30);
System.out.println(p.getAge()); // 출력: 30
}
}
이 예시에서는 Person 클래스에서 캡슐화(Encapsulation)가 적용되어, name과 age 같은 속성은 private으로 선언되어 외부에서 직접 접근할 수 없으며, 이를 조작하기 위해서는 제공된 메서드(getName(), setAge())를 통해 간접적으로 접근하게 됩니다.
3️⃣ 상속(Inheritance)
상속(Inheritance)은 기존 클래스를 확장하여 새로운 클래스를 만드는 방법입니다.
자식 클래스는 부모 클래스의 속성과 매서드를 물려받아 재사용하며, 필요하면 추가로 기능을 확장하거나 수정할 수 있습니다.
상속(Inheritance)을 통해 코드의 재사용성을 높이고, 계층 구조를 만들 수 있습니다.
👉 예시
// 부모 클래스.
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println("동물이 소리를 냅니다.")
}
}
// 자식 클래스 (상속)
class Dog extends Animal {
public Dog(String name) {
super(name); // 부모 클래스의 생성자를 호출
}
// 부모 메서드 오버라이딩
@Override
public void makeSound() {
System.out.println(name + "이(가) 멍멍 짖습니다.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("나르");
dog.makeSound(); // 출력: 나르이(가) 멍멍 짖습니다.
}
}
위 예시에서, Dog 클래스(Class)는 Animal 클래스(Class)를 상속받아 makeSound() 메서드를 재정의(Overriding, 오버라이딩)하고, 이름을 출력하도록 확장했습니다.
상속(Inheritance)을 통해 부모 클래스의 기능을 재사용하면서도 필요에 따라 추가적인 기능을 구현할 수 있습니다.
4️⃣ 다형성(Polymorphism)
다형성(Polymorphism)은 같은 이름의 메서드가 다양한 방법으로 동작할 수 있게 하는 기능입니다.
상속(Inheritance) 관계에서 부모 클래스(Parents class, Super class)의 메서드(Methods)를 자식 클래스(Child class, Sub class)에서 재정의(오버라이딩, Overriding)하여 다른 방식으로 동작하게 하거나, 같은 이름의 메서드가 서로 다른 매개변수(Paremeter)에 따라 다르게 동작(오버로딩, Overloading)할 수 있습니다.
다형성(Polymorphism)을 통해 코드의 유연성과 확장성을 높일 수 있습니다.
👉 예시.
class Animal {
public void makeSound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("멍멍");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("야옹");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // 출력: 멍멍
animal2.makeSount(); // 출력: 야옹
}
}
이 예시는 다형성(Polymorphism)을 보여주는 좋은 예입니다.
Animal 타입의 변수에 Dog와 Cat 객체를 할당할 수 있으며, makeSound() 메서드를 호출하면 객체의 타입에 맞게 다르게 동작합니다.
이처럼 부모 클래스 타입으로 다양한 자식 객체를 처리할 수 있습니다.
5️⃣ 추상화(Abstraction)
추상화(Abstraction)은 복잡한 현실의 객체에서 필요한 부분만을 모델링하여 객체로 표현하는 과정입니다.
불필요한 세부 사항은 숨기고 중요한 부분만 드러내어 효율적으로 문제를 해결하는 데 도움을 줍니다.
추상화(Abstraction)는 인터페이스(Interface)나 추상 클래스를 통해 구현됩니다.
👉 예시.
// 추상 클래스
abstract class Vehicle {
String model;
public Vehicle(String model) {
this.model = model;
}
// 추상 메서드 (구체적인 구현은 하위 클래스에서)
public abstract void move();
}
// 자식 클래스 (구체적인 구현 제공)
class Car extends Vehicle {
public Car(String model) {
super(model);
}
@Override
public void move() {
System.out.println(model + "이(가) 도로에서 달립니다.");
}
}
class Airplane extends Vehicle {
public Airplane(String model) {
super(model);
}
@Override
public void move() {
System.out.println(model + "이(가) 하늘을 납니다.");
}
}
public class Main {
public static void main(String[] args) {
Vehicle car = new Car("Volvo xc60");
Vehicle airplane = new Airplane("Boeing 747");
car.move(); // 출력: Volvo xc60이(가) 도로에서 달립니다.
airplane.move(); // 출력: Boeing 747이(가) 하늘을 납니다.
}
}
위 예시에서는 추상 클래스(Abstract class)를 통해서 추상화(Abstraction)를 보여줍니다.
Vehicle 클래스는 추상 클래스(Abstract class)이며, 자식 클래스인 Car 와 Airplane이 구체적인 구현을 제공합니다.
추상화(Abstraction)를 통해 공통적인 동작을 정의하면서도 각 객체가 개별적인 동작을 구현할 수 있습니다.
6️⃣ 갈무리.
이러한 개념을 바탕으로 객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 코드의 재사용성을 높이고, 유지보수와 확장성을 개선하며, 현실 세계의 문제를 더 직관적으로 해결할 수 있도록 합니다.
-
🌐[Network] 패킷(Packet)이란 무엇일까요?
🌐[Network] 패킷(Packet)이란 무엇일까요?
패킷(Packet)은 네트워크 상에서 데이터를 전송하는 기본 단위입니다.
컴퓨터 네트워크에서 큰 데이터를 전송 할 때, 데이터를 작은 조각으로 나누어 각각의 조각을 패킷(Packet)이라고 부릅니다.
이러한 패킷(Packet)은 네트워크를 통해 목적지에 도달하며, 도착한 후에 다시 원래의 데이터로 재조립됩니다.
1️⃣ 패킷의 필요성.
데이터를 전송할 때 단일 큰 데이터 덩어리로 보내는 대신, 데이터를 여러 개의 작은 패킷으로 나누는 이유는 네트워크의 효율성과 안정성을 높이기 위해서 입니다.
1️⃣ 효율성.
데이터를 작은 단위로 나누어 전송하면 네트워크 리소스를 더 효율적으로 사용할 수 있습니다.
대용량 데이터가 네트워크에서 하나의 덩어리로 전송된다면, 다른 데이터가 전송되는 동안 대기해야 할 수 있습니다.
패킷(Packet)은 나뉘어 전송되므로 여러 데이터가 동시에 네트워크를 통해 흐를 수 있습니다.
2️⃣ 안정성.
작은 패킷(Packet) 단위로 나뉘어 전송하면 데이터 손실이 발생했을 때 일부 패킷(Packet)만 재전송하면 되므로, 전체 데이터를 다시 전송하지 않아도 됩니다.
네트워크 연결이 불안정하거나 문제가 발생했을 때도, 손상된 패킷(Packet)만 다시 보내 효율적으로 문제를 해결할 수 있습니다.
3️⃣ 경로 다양성.
네트워크를 통해 전송되는 패킷(Packet)은 서로 다른 경로를 통해 목적지에 도달할 수 있습니다.
즉, 같은 데이터를 여러 경로로 나눠 전송하므로 특정 경로에 문제가 생겨도 다른 경로를 통해 전달될 수 있습니다.
2️⃣ 패킷의 구조.
패킷(Packet)은 크게 헤더(Header)와 데이터(Payload, 페이로드)로 구성됩니다.
1️⃣ 헤더(Header)
패킷(Packet)의 헤더(Header)는 제어 정보를 담고 있으며, 네트워크에서 패킷(Packet)이 어떻게 전달되고 처리될지 결정하는 데 중요한 역할을 합니다.
헤더(Header)에는 다음과 같은 정보가 포함됩니다.
출발지 IP(Internet Protocol) 주소 : 패킷(Packet)을 보낸 컴퓨터의 IP(Internet Protocol) 주소.
목적지 IP(Internet Protocol) 주소 : 패킷(Packet)이 도착해야 할 컴퓨터 IP(Internet Protocol) 주소.
프로토콜(Protocol) : 패킷(Packet)이 어떤 프로토콜(예: TCP(Transmission Control Protocol), UDP(User Datagram Protocol))을 사용하는지 나타냅니다.
패킷 번호 : 패킷이 전체 데이터에서 몇 번째 패킷인지를 나타냅니다.
TTL(Time to Live) : 패킷이 네트워크에서 얼마 동안 유효한지를 나타내며, 특정 시간이나 홉(hop)을 넘으면 패킷(Packet)은 폐기됩니다.
📝 홉(hop)
네트워크에서 패킷(Packet)이 한 네트워크 장비(주로 라우터)에서 다른 장비로 이동하는 과정을 의미하는 단위입니다.
네트워크 패킷이 출발지에서 목적지까지 이동하는 동안 라우터와 같은 중간 장비들을 통과할 때마다 하나의 홉이 발생합니다.
2️⃣ 데이터(Payload, 페이로드)
패킷(Packet)의 데이터 부분은 실제로 전송하려는 유효 데이터(문서, 이미지, 동영상 파일의 일부 등)가 담긴 부분입니다.
이 데이터는 네트워크 상에서 목적지에 도달한 후, 여러 패킷이 다시 모여 원래의 데이터로 재구성 됩니다.
3️⃣ 트레일러(Trailer)
일부 패킷(Packet) 구조에서 트레일러(Trailer)가 포함될 수 있습니다.
이는 패킷(Packet)이 올바르게 전송되었는지 확인하기 위한 오류 검사 정보를 포함합니다.
예를 들어, 체크섬(Checksum)이 포함되어 패킷이 손상되었는지 확인할 수 있습니다.
📝 체크섬(Checksum)
데이터의 무결성을 확인하기 위해 사용되는 오류 검출 기법 중 하나입니다.
데이터가 전송되거나 저장될 때, 손상되거나 변경된 부분이 없는지 확인하기 위해 사용됩니다.
체크섬(Checksum)은 전송하려는 데이터에 대해 수학적인 계산을 수행한 후 고유한 값(체크섬 값)을 생성하고, 이 값을 데이터와 함께 전송하거나 저장합니다.
데이터를 수신하는 측에서는 동일한 방식으로 체크섬(Checksum) 값을 계산하여 원래의 체크섬 값과 비교함으로써 데이터가 정상적으로 전송되었는지 확인할 수 있습니다.
3️⃣ 패킷의 동작 원리.
1️⃣ 데이터 나누기.
네트워크를 통해 데이터를 전송할 때, 원래의 데이터를 여러 개의 작은 패킷으로 분할합니다.
각 패킷(Packet)은 헤더(Header)와 데이터(Payload, 페이로드)를 포함하며, 헤더(Header)에는 각 패킷(Packet)이 어디에서 어디로 가는지, 어떤 경로를 따라가야 하는지 등의 정보가 담깁니다.
2️⃣ 네트워크 전송.
패킷(Packet)은 네트워크를 통해 목적지로 전송됩니다.
이때, 패킷(Packet)은 네트워크 장치(라우터(Router, Switch 등)를 거쳐 목적지까지 전달됩니다.
각 장치는 패킷의 헤더(Header)를 읽어 적절한 경로를 찾아 다음 목적지로 패킷(Packet)을 보냅니다.
3️⃣ 데이터 재조립.
목적지에 도착한 패킷(Packet)들은 다시 조립되어 원래의 데이터를 복구합니다.
전송 중에 일부 패킷(Packet)이 손실되거나 손상된 경우, 해당 패킷(Packet)만 다시 요청하여 전송받을 수 있습니다.
4️⃣ 패킷 교환과 전송 방식.
네트워크에서 패킷 전송 방식을 패킷 교환(Packet Switching)이라고 합니다.
이는 데이터를 패킷(Packet) 단위로 나누어 각 패킷(Packet)이 독립적으로 네트워크를 통해 전송되는 방식입니다.
1️⃣ 패킷 교환(Packet Switching)
패킷 교환 방식은 데이터를 작은 패킷(Packet) 단위로 나누어, 각 패킷이 네트워크를 통해 독립적으로 전송됩니다.
각 패킷(Packet)은 목적지에 도달할 때까지 다른 경로를 통해 이동할 수 있으며, 목적지에서 다시 재조립됩니다.
2️⃣ 회선 교환(Circuit Switching)
회선 교환 방식은 전화 통화와 같은 네트워크에서 사용되며, 데이터를 전송하기 위해 고정된 경로를 설정한 후 데이터를 전송합니다.
데이터 전송이 완료될 때까지 해당 경로는 다른 용도로 사용할 수 없습니다.
3️⃣ 데이터그램 기반 전송.
UDP(User Datagram Protocol)는 패킷 교환 방식 중 하나로, 데이터가 순서에 상관없이 패킷 단위로 전송되며, 수신자는 도착한 패킷을 기반으로 데이터를 복원합니다.
UDP(User Datagram Protocol)는 빠른 전송 속도가 필요하지만 데이터 정확도가 덜 중요한 경우(예: 실시간 스트리밍, 게임)에 사용됩니다.
5️⃣ 패킷과 네트워크 프로토콜
패킷은 여러 네트워크 프로토콜에 의해 관리됩니다.
주로 사용하는 프로토콜은 다음과 같습니다.
1️⃣ TCP(Transmission Control Protocl) / IP(Internet Protocol)
TCP(Transmission Control Protocol) / IP(Internet Protocol)는 인터넷 프로토콜로, 패킷(Packet)이 정확하게 도착하는지 확인하고, 손실된 패킷이 있으면 다시 전송하는 방식입니다.
TCP(Transmission Control Protocol)는 데이터의 신뢰성을 보장하고, IP(Internet Protocol)는 패킷(Packet)의 주소 지정과 경로 선택을 담당합니다.
2️⃣ UDP(User Datagram Protocol)
UDP(User Datagram Protocol)는 TCP(Transmission Control Protocol)와 달리 비연결형 프로토콜로, 패킷(Packet)의 신뢰성을 보장하지 않습니다.
속도가 중요한 경우, 예를 들어 동영상 스트리밍, 온라인 게임에서 많이 사용됩니다.
6️⃣ 패킷 손실과 복구
1️⃣ 패킷 손실
네트워크에서 패킷이 소실되거나 지연될 수 있습니다.
이는 네트워크 혼잡, 라우터의 버퍼 부족, 신호 간섭 등 다양한 이유로 발생할 수 있습니다.
📝 라우터의 버퍼(Buffer)
라우터의 버퍼(Buffer)는 라우터가 처리해야 할 데이터 패킷(Packet)들을 임시로 저장하는 메모리 공간을 의미합니다
라우터는 네트워크를 통해 전달되는 수많은 패킷을 처리하고 전달하는 역할을 하며, 네트워크 혼잡이나 트래픽 변동 상황에서 모든 패킷을 즉시 처리할 수 없을 때 이 버퍼에 패킷을 일시적으로 저장합니다.
2️⃣ 패킷 복수
TCP와 같은 신뢰성 있는 프로토콜은 패킷 손실이 발생했을 때 손실된 패킷을 재전송합니다.
그러나 UDP는 패킷 손실이 발생해도 이를 처리하지 않으며, 손실된 데이터를 그대로 넘깁니다.
7️⃣ 패킷의 예시.
👉 웹 브라우징.
사용자가 웹 브라우저에서 특정 웹사이트에 접속할 때, 웹 서버는 HTML, CSS, 이미지 등의 파일을 패킷(Packet)으로 나누어 사용자의 컴퓨터로 전송합니다.
사용자의 컴퓨터는 이 패킷들을 수신하고, 다시 조립하여 웹 페이지를 화면에 표시합니다.
👉 이메일 전송.
이메일이 전송될 때, 이메일 본문과 첨부 파일은 여러 개의 패킷으로 나누어 네트워크를 통해 전송됩니다.
각 패킷은 목적시 이메일 서버에 도착하면 다시 하나의 이메일로 합쳐집니다.
8️⃣ 결론.
패킷(Packet)은 네트워크에서 데이터를 전송하기 위해 작게 나눈 데이터 조각으로, 네트워크 통신의 효율성과 신뢰성을 높이는 데 중요한 역할을 합니다.
패킷 스위칭 방식은 데이터를 독립적인 패킷으로 나누어 네트워크 경로를 유연하게 선택해 전송하며, 이를 통해 인터넷을 포함한 다양한 네트워크에서 데이터 통신의 효율성을 극대화합니다.
패킷은 데이터를 전송할 때 헤더, 데이터, 오류 검출 정보 등을 포함하며, 목적지에서 재조립되어 원래의 데이터로 복구됩니다.
-
🌐[Network] 라우터와 스위치의 차이점.
🌐[Network] 라우터(Router)와 스위치(Switch)의 차이점.
라우터(Router)와 스위치(Switch)는 네트워크 장비로서, 모두 데이터를 전달하고 네트워크 장치들을 연결하는 역할을 하지만, 기능과 동작 방식에서 중요한 차이점이 있습니다.
주로 어느 계층에서 동작하는지, 어떤 정보를 바탕으로 데이터를 전송하는지, 그리고 어떤 네트워크 환경에서 사용되는지에 따라 구분됩니다.
1️⃣ 기본 역할.
👉 라우터(Router)
다른 네트워크 간의 통신을 담당합니다.
라우터(Router)는 IP 주소를 사용하여 패킷(Packet)을 목적지 네트워크로 라우팅(Routing)하며, 주로 LAN(Local Area Network, 근거리 통신망)과 WAN(Wide Area Network, 광역 통신망) 간의 연결을 담당합니다.
즉, 네트워크와 네트워크를 연결하는 장치입니다.
🙋♂️ IP(Internet Protocol)란 무엇일까?
🙋♂️ WAN(Wide Area Network)이란 무엇일까요?
🙋♂️ LAN이 합쳐져서 WAN이 되는걸까?
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
👉 스위치(Switch)
같은 네트워크 내 장치들 간의 통신을 관리합니다.
스위치(Switch)는 MAC(Media Access Control) 주소를 기반으로 데이터를 전송하며, LAN(Local Area Network, 근거리 통신망) 내에서 컴퓨터, 프린터, 서버 등의 장치들이 서로 통신할 수 있도록 패킷(Packet)을 전달하는 역할을 합니다.
즉, 장치와 장치를 연결하는 장치입니다.
🙋♂️MAC(Media Access Control)주소란 무엇일까요?
2️⃣ 작동하는 계층.
👉 라우터(Router)
OSI 7계층 모델의 3계층(네트워크 계층)에서 동작합니다.
라우터(Router)는 IP(Internet Protocol) 주소를 바탕으로 패킷(Packet)의 라우팅(Routing)을 수행하며, 서로 다른 네트워크 간의 데이터 전송 경로를 설정하고 관리합니다.
👉 스위치(Switch)
OSI 7 계층 모델의 2계층(데이터 링크 계층)에서 동작합니다.
스위치(Switch)는 MAC(Media Access Control) 주소를 사용하여 데이터를 목적지 장치로 전달합니다.
스위치(Switch)는 장치 간 데이터 프레임을 전송하고, 네트워크 내에서 패킷(Packet)을 필터링하고 전송할 경로를 설정합니다.
🙋♂️ OSI 7계층 모델
🙋♂️ OSI 7계층 모델 - 계층별 기능
🙋♂️ LAN 내에서 전달되는 데이터 프레임이란 무엇일까?
3️⃣ 주소 기반 동작.
👉 라우터(Router)
IP(Internet Protocol) 주소를 사용하여 네트워크 장치 간에 패킷(Packet)을 전달합니다.
출발지 IP(Internet Protocol) 주소와 목적지 IP(Internet Protocol) 주소를 분석하여, 데이터를 최적의 경로로 라우팅(Routing)합니다.
라우터(Router)는 네트워크 간의 트래픽을 제어하고, 인터넷과 같은 WAN(Wide Area Network) 연결을 관리합니다.
👉 스위치(Switch)
MAC(Media Address Control) 주소를 사용하여 데이터 프레임을 전달합니다.
스위치(Switch)는 MAC(Media Access Control) 주소 테이블을 유지하며, 패킷(Packet)의 목적지 MAC(Media Access Control) 주소에 따라 해당 프레임을 적절한 포트로 전송합니다.
주로 LAN(Local Area Network, 근거리 통신망) 내에서 사용됩니다.
🙋♂️ 네트워크 포트(Network Port)란 무엇일까?
🙋♂️ 네트워크, 포트, 도메인 이름, IP, DNS
4️⃣ 주요 기능.
👉 라우터(Router)
서로 다른 네트워크(LAN(Local Area Network, 근거리 통신망)과 WAN(Wide Area Network, 광역 통신망)) 간의 데이터 패킷(Packet) 전달 및 라우팅(Routing)
인터넷 연결을 공유하고, 다양한 네트워크를 연결하는 역할.
네트워크 트래픽 제어 및 IP(Internet Protocol) 주소 할당(DHCP), 네트워크 주소 변환(NAT)
방화벽(Firewall) 기능을 통해 보안 제어 가능.
👉 스위치(Switch)
LAN(Local Area Network, 근거리 통신망) 내에서 여러 장치 간의 데이터를 빠르게 전송.
MAC(Media Access Control) 주소 기반으로 데이터를 필터링하고 적절한 포트(Port)로 전송.
네트워크(Network) 내 장치 간의 충돌 방지 및 효율적인 데이터 전송.
VLAN(Virtual LAN)을 설정하여 네트워크 세분화 가능.
📝 VLAN(Virtual LAN, 가상 LAN)
하나의 물리적인 네트워크 장비, 예를 들어 스위치 내에서 네트워크를 논리적으로 분리하여 여러 개의 가상 네트워크를 만드는 기술입니다.
VLAN(Virtual LAN, 가상 LAN)은 서로 다른 네트워크 세그먼트에 속하는 장치들을 같은 스위치에 연결되어 있어도 서로 통신할 수 없도록 분리하거나, 같은 VLAN에 속한 장치들끼리만 통신할 수 있도록 합니다.
📝 Virtual Network(가상 네트워크)
물리적인 네트워크 하드웨어와 상관없이, 소프트웨어적으로 구현된 네트워크를 의미합니다.
가상 네트워크는 물리적인 네트워크 인프라를 추상화하여, 네트워크의 연결과 관리가 소프트웨어적으로 처리되도록 구성됩니다.
이러한 가상 네트워크는 서버, 네트워크 장비, 클라이언트 등을 물리적으로 연결하지 않고도 논리적으로 네트워크를 생성하고 관리할 수 있게 해줍니다.
5️⃣ 사용되는 네트워크 범위.
👉 라우터(Router)
다른 네트워크 간의 연결을 관리하므로, 주로 WAN(Wide Area Network, 광역 통신망)이나 인터넷 연결이 필요한 환경에서 사용됩니다.
예를 들어, 가정에서는 라우터(Router)가 인터넷 서비스 제공업체(ISP, Internet Service Provider)와 사용자의 네트워크를 연결하는 역할을 수행합니다.
👉 스위치(Switch)
같은 네트워크 내에서 장치들을 연결합니다.
주로 LAN(Local Area Network, 근거리 통신망)에서 사용되며, 컴퓨터, 프린터, 서버 등 다양한 장치가 동일한 네트워크 내에서 데이터를 주고받는 데 사용됩니다.
6️⃣ 전송 방식.
👉 라우터(Router)
패킷 스위칭(Packet Switching)을 사용합니다.
라우터(Router)는 패킷(Packet)이 어디로 갈지에 대한 최적의 경로를 계산하고, 각 패킷이 다른 경로로 이동할 수 있게 합니다.
👉 스위치(Switch)
프레임 스위칭(Frame Switching)을 사용합니다.
스위치(Switch)는 목적지 MAC(Media Address Control) 주소를 기반으로 데이터 프레임을 전송하고, 네트워크 내 장치 간의 빠른 데이터 전송을 보장합니다.
7️⃣ 브로드캐스트 도메인과 충돌 도메인.
👉 라우터(Router)
브로드캐스트 도메인(Broadcast Domain)을 분리합니다.
라우터(Router)는 네트워크를 나누기 때문에, 라우터(Router)를 경우한 네트워크 간에는 브로드캐스트 트래픽이 전달되지 않습니다.
👉 스위치(Switch)
브로드캐스트 도메인(Broadcast Domain)을 나누지 않고, 충돌 도메인(Collision Domain)만 분리합니다.
스위치(Switch)는 연결된 모든 장치가 동일한 브로드캐스트 도메인에 속해 있으므로, 브로드캐스트 트래픽이 스위치(Switch)를 경유하여 네트워크(Network) 전체에 전달됩니다.
📝 브로드캐스트 도메인(Broadcast Domain)
브로드캐스트 도메인(Broadcast Domain)은 네트워크 내에서 브로드캐스트 트래픽이 전파되는 범위를 의미합니다.
다시 말해, 네트워크 장치가 브로드캐스트(Broadcast) 메시지(모든 네트워크 장치에 전송되는 메시지)를 보냈을 때, 그 메시지를 받을 수 있는 네트워크 상의 모든 장치들이 속한 영역을 브로드캐스트 도메인(Broadcast Domain)이라고 합니다.
📝 충돌 도메인(Collision Domain)
충돌 도메인(Collision Domain)은 네트워크에서 두 개 이상의 장치가 동시에 데이터를 전송할 때 충돌이 발생할 수 있는 네트워크 범위를 의미합니다.
충돌이란 동시에 전송된 데이터 패킷들이 서로 켭치거나 부딪히는 상황을 말하며, 충돌 도메인 내에서는 한 번에 하나의 장치만 데이터를 전송해야 충돌을 피할 수 있습니다.
네트워크 장비가 데이터 충돌을 처리할 수 없는 환경에서는 충돌이 발생하면 데이터는 손실되거나 다시 전송해야 하므로, 네트워크 성능에 영향을 미칠 수 있습니다.
8️⃣ 보안 기능.
👉 라우터(Router)
보안 기능이 더 강력합니다.
라우터는 방화벽(Firewall), NAT(Network Address Translation) 등을 통해 외부 네트워크로부터 보호하고, 네트워크 트래픽을 모니터링하고 제어할 수 있습니다.
👉 스위치(Switch)
기본적으로 보안 기능이 적습니다.
스위치는 내부 네트워크에서 장치 간의 데이터를 전송하므로, 보안 측면에서 라우터만큼 강력한 보호 기능을 제공하지는 않습니다.
그러나 고급 스위치에서는 VLAN(Virtual LAN, 가상 LAN)을 통해 네트워크를 분리하고, 포트 보안을 설정할 수 있습니다.
9️⃣ 주요 차이점 요약.
기능
라우터(Router)
스위치(Switch)
작동 계층
네트워크 계층(3계층, IP 주소 사용)
데이터 링크 계층(2계층, MAC 주소 사용)
사용하는 주소
IP 주소
MAC 주소
기본 역할
서로 다른 네트워크를 연결(LAN과 WAN간)
동일 네트워크 내의 장치들을 연결(LAN 내)
보안 기능
방화벽, NAT, 트래픽 제어 등 고급 보안 기능 제공제한적(고급 스위치 VLAN 및 포트 보안 사용 가능)
제한적(고급 스위치에서 VLAN 및 포트 보안 사용 가능)
브로드캐스트 도메인
분리
분리하지 않음
주요 사용 환경
WAN, 인터넷 연결, 서로 다른 네트워크 간 데이터 전송
LAN 내에서의 데이터 전송 및 네트워크 장치 연결
전송 방식
패킷 스위칭(IP 패킷)
프레임 스위칭(데이터 프레임)
1️⃣0️⃣ 결론.
라우터(Router)는 서로 다른 네트워크 간의 연결을 관리하며, IP 주소를 통해 네트워크 간의 경로를 설정하고 데이터를 전송하는 역할을 합니다.
반면, 스위치(Switch)는 LAN(Local Area Network, 근거리 통신망) 내의 장치들을 연결하고 MAC(Media Access Control) 주소를 통해 데이터 프레임을 전송합니다.
즉, 라우터(Router)는 네트워크 간의 통신을, 스위치(Switch)는 네트워크 내부 장치들 간의 통신을 담당합니다.
-
💾[Database] 관계형 데이터베이스(Relational Database, RDB)란 무엇일까요?
💾[Database] 관계형 데이터베이스(Relational Database, RDB)란 무엇일까요?
관계형 데이터베이스(Relational Database, RDB)는 테이블 형식으로 데이터를 저장하고, 테이블 간의 관계를 정의하여 데이터를 관리하는 데이터 모델입니다.
이를 관리하기 위해 데이터베이스 관리 시스템(Database Management System, DBMS)이 사용되며, RDB(Relational Database, 관계형 데이터베이스)를 관리하는 DBMS(Database Management System, 데이터베이스 관리 시스템)는 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)이라고 합니다.
관계형 데이터베이스(Relational Database, RDB)는 데이터를 행(Row)과 열(Column)로 구성된 테이블(Table)에 저장되며, 각 테이블은 서로 관계를 맺어 구조화된 데이터를 관리하고 검색하기 쉽게 합니다.
1️⃣ 관계형 데이터베이스(RDB, Relational Database)의 주요 특징.
1️⃣ 테이블 구조.
관계형 데이터베이스(RDB, Relational Database)는 데이터를 테이블 형태로 저장합니다.
각 테이블은 행(Row)과 열(Column)로 구성되며, 행(Row)은 레코드(Record)라고 하고, 열(Column)은 속성(Attribute) 또는 필드(Field)라고 부릅니다.
예를 들어, “사용자”라는 테이블이 있다면, 각 행(Row)은 사용자 정보(이름, 이메일, 나이 등)를 나타내고, 각 열(Column)은 사용자 정보를 설명하는 속성(Attribute)을 나타냅니다.
2️⃣ 관계.
관계형 데이터베이스(Relational Database, RDB)는 여러 테이블 간의 관계를 정의할 수 있습니다.
키(Key)를 사용해 테이블 간의 관계를 맺고, 데이터를 참조하고 연결할 수 있습니다.
예를 들어, “사용자” 테이블과 “주문” 테이블이 있다고 하면, “사용자” 테이블의 기본 키(Primary Key)가 “주문” 테이블에 외래 키(Foreign Key)로 사용되며, 사용자와 주문 간의 관계를 나타낼 수 있습니다.
3️⃣ 고유한 키(Primary Key).
각 테이블에는 고유한 키(Primary Key)가 있습니다.
이 키는 테이블의 각 레코드(Record)를 유일하게 식별하는 역할을 합니다.
기본 키(Primary Key)는 중복될 수 없으며, 이를 통해 데이터를 정확하게 검색하고 관리할 수 있습니다.
4️⃣ 데이터 무결성 및 일관성.
관계형 데이터베이스(Relational Database, RDB)는 데이터 무결성(Integrity)과 일관성(Consistency)을 유지하도록 설계되었습니다.
이를 통해 데이터의 정확성과 신뢰성을 보장할 수 있습니다.
제약 조건(Constraints)을 통해 데이터의 무결성(Integrity)을 유지합니다.
예를 들어, NOT NULL 제약 조건을 사용하여 특정 열이 항상 값을 가져야 한다는 것을 보장하거나, FOREIGN KEY 제약 조건을 사용해 테이블 간의 관계를 유지합니다.
5️⃣ SQL(Structured Query Language)
관계형 데이터베이스(Relational Database, RDB)는 데이터를 관리하기 위해 SQL(Structured Query Language)이라는 언어를 사용합니다.
SQL(Structured Query Language)은 데이터를 조회, 삽입, 수정, 삭제하는 데 사용되는 표준 언어입니다.
SQL(Structured Query Language)은 관계형 데이터베이스(Relational Database, RDB)에서 데이터 검색과 조작을 위한 강력한 도구입니다.
예: 데이터를 삽입하기 위한 INSERT문, 데이터를 조회하기 위한 SELECT문, 데이터를 수정하기 위한 UPDATE문, 데이터를 삭제하기 위한 DELETE문 등이 있습니다.
2️⃣ 관계형 데이터베이스(Relational Database, RDB)의 구성 요소.
1️⃣ 테이블(Table)
관계형 데이터베이스(Relational Database, RDB)의 기본 단위입니다.
데이터는 행(Row)과 열(Column)로 구성된 테이블에 저장됩니다.
예: 사용자(Users) 테이블, 주문(Order) 테이블
2️⃣ 기본 키(Primary Key)
각 테이블에서 고유하게 레코드를 식별하는 열(Column)입니다.
중복될 수 없으며, 각 행(Row)을 유일하게 구분할 수 있습니다.
예: 사용자 테이블의 user_id 열(Column)은 기본 키(Primary Key)로 사용될 수 있습니다.
3️⃣ 외래 키(Foreign Key)
다른 테이블의 기본 키(Primary Key)를 참조하는 열(Column)로, 테이블 간의 관계를 정의합니다.
외래 키(Foreign Key)는 두 테이블을 연결하는 역할을 하며, 데이터 간의 일관성을 유지하는 데 도움을 줍니다.
예: 주문(Orders) 테이블에서 user_id 열이 사용자(Users) 테이블의 user_id를 참조하는 경우, 이를 외래키(Foreign Key)라고 합니다.
4️⃣ 속성(Attribute)
테이블의 열(Column)을 의미하며, 각 속성(Attribute)은 특정 유형의 데이터를 저장합니다.
예를 들어, 사용자 테이블에서 name, email, age와 같은 속성이 있을 수 있습니다.
5️⃣ 레코드(Record)
테이블의 행(Row)을 의미하며, 하나의 레코드는 테이블에 저장된 데이터의 한 항목을 나타냅니다.
예를 들어, 사용자 테이블의 한 행(Row)은 한 사용자의 정보를 나타냅니다.
3️⃣ 관계형 데이터베이스(Relational Database, RDB)의 예시.
예를 들어, 전자상거래 웹사이트를 위한 데이터베이스를 설계한다고 가정해봅시다.
여기에는 사용자와 주문 정보를 저장하기 위해 두 개의 테이블이 있을 수 있습니다.
1️⃣ 사용자(Users) 테이블.
user_id(기본 키, Primary Key)
name`
email
address
2️⃣ 주문(Orders) 테이블.
order_id(기본 키, Primary Key)
user_id(외래 키, Foreign Key, 사용자와의 관계를 나타냄)
product
quantity
3️⃣ 설명.
이 예에서, 사용자 테이블과 주문 테이블은 user_id를 통해 관계를 맺고 있습니다.
이를 통해 특정 사용자가 어떤 주문을 했는지 쉽게 조회할 수 있습니다.
4️⃣ 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)의 예.
MySQL
오픈 소스 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)으로, 웹 애플리케이션에서 많이 사용됩니다.
PostgreSQL
오픈 소스 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)로, 확장성과 표준 준수에 중점을 둔 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)입니다.
Oracle
대규모 상업용 데이터베이스로, 높은 성능과 보안성을 자랑하는 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)입니다.
Microsoft SQL Server
마이크로소프트에서 개발한 관계형 데이터베이스 관리시스템(RDBMS, Relational Database Management System)으로, 기업 환경에서 많이 사용됩니다.
5️⃣ 관계형 데이터베이스(Relational Database, RDB)의 장점.
1️⃣ 데이터 무결성 보장.
관계형 데이터베이스(Relational Database, RDB)는 제약 조건(Constraints)을 통해 데이터의 무결성을 보장합니다.
예를 들어, 외래 키(Foreign key)를 통해 테이블 간의 참조 무결성을 유지하고, 데이터의 일관성을 확보합니다.
2️⃣ SQL을 통한 데이터 관리.
관계형 데이터베이스(Relational Database)는 SQL(Structured Query Language)이라는 표준 언어를 사용하여 데이터를 검색, 삽입, 수정, 삭제할 수 있습니다.
SQL은 관계형 데이터베이스에서 데이터를 쉽게 관리할 수 있도록 해줍니다.
3️⃣ 데이터 중복 최소화.
관계형 데이터베이스(Relational Database, RDB)는 데이터를 여러 테이블로 나누고, 중복을 최소화하여 저장합니다.
이로 인해 데이터 저장소의 효율성이 증가하고, 데이터 일관성을 유지할 수 있습니다.
4️⃣ 데이터 보안.
관계형 데이터베이스(Relational Database, RDB)는 사용자 권한을 관리하여, 데이터에 대한 접근을 제어하고 보안을 강화할 수 있습니다.
6️⃣ 관계형 데이터베이스의 단점.
1️⃣ 복잡한 구조.
데이터가 여러 테이블에 분산되어 저장되기 때문에, 데이터의 구조가 복잡해질 수 있습니다.
특히, 데이터간의 관계가 많아질수록 관리와 설계가 어려워질 수 있습니다.
2️⃣ 확장성의 한계.
관계형 데이터베이스(Relational Database, RDB)는 데이터의 수평적 확장(데이터를 여러 서버로 나누어 저장)이 어려운 경우가 많습니다.
데이터가 크고 관계가 복잡할수록 확장성과 성능에 제약이 있을 수 있습니다.
3️⃣ 고정된 스키마.
관계형 데이터베이스(Relational Database, RDB)는 고정된 스키마를 사용합니다.
즉, 테이블 구조(열(Row)의 수와 이름 등)가 정해진 후, 이를 변경하기 어렵습니다.
데이터 구조가 자주 변경되는 경우 유연성이 떨어질 수 있습니다.
7️⃣ 관계형 데이터베이스(Relational Database, RDB)와 NoSQL 데이터베이스의 차이.
1️⃣ 관계형 데이터베이스(Relational Database, RDB)
데이터를 테이블 형태로 저장하며, 테이블 간의 관계를 정의합니다.
SQL(Structured Query Language)을 사용하여 데이터를 관리합니다.
일관성과 무결성을 중요하게 다루며, 고정된 스키마 구조를 가집니다.
2️⃣ NoSQL 데이터베이스
데이터를 유연한 구조로 저장하며, 문서(Document), 키-값(Key-Value), 그래프(Graph) 등 다양한 방식으로 데이터를 저장할 수 있습니다.
스키마가 유연하여 데이터 구조가 자주 변경될 때 유리합니다.
관계형 데이터베이스(Relational Database, RDB)에 비해 수평적 확장이 용이하며, 대규모 분산 시스템에 적합합니다.
8️⃣ 결론.
관계형 데이터베이스(Relational Database, RDB)는 데이터를 테이블 형태로 저장하고, 여러 테이블 간의 관계를 정의하여 데이터를 효율적으로 관리하는 데이터베이스 관리 시스템입니다.
SQL을 사용해 데이터를 관리하며, 데이터 무결성과 일관성을 유지하는 데 강점을 가집니다.
대표적인 관계형 데이터베이스로는 MySQL, PostgreSQL, Oracle 등이 있으며, 복잡한 관계를 가지는 정형 데이터 관리에 적합합니다.
-
💾 [CS] 객체 지향 프로그래밍에서의 객체(Object)란 무엇일까요?
💾 [CS] 객체 지향 프로그래밍에서의 객체(Object)란 무엇일까요?
객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서의 객체(Object)는 클래스(Class)에 의해 정의된 데이터와 그 데이터를 처리하는 동작(메소드, Method)을 포함하는 독립적인 개체입니다.
객체(Object)는 프로그램 내에서 상태(속성 또는 필드)와 행위(메소드 또는 함수)를 가지며, 이러한 상태와 행위를 통해 현실 세계의 사물을 모델링하거나 시스템 내의 개념을 추상화하는 방식으로 사용됩니다.
🙋♂️ 추상화(Abstraction)
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’이란 무엇일까?
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.
1️⃣ 객체의 구성 요소.
1️⃣ 속성(Attributes) 또는 필드(Fields)
객체의 데이터를 나타냅니다.
이는 객체가 가지는 상태를 표현하는 변수로, 클래스에서 정의된 속성(Attributes)에 따라 각 객체는 고유한 값을
예를 들어, “자동차” 객체에는 색상, 속도 같은 속성이 있을 수 있습니다.
Java에서 속성은 인스턴스 변수로 표현됩니다.
class Car {
private String color;
private int speed;
public Car(String color, int speed) {
this.color = color; // 속성
this.speed = this.speed; // 속성
}
}
2️⃣ 메소드(Methods)
객체의 행동을 정의하는 함수입니다.
메소드는 객체의 데이터를 조작하거나 특정 작업을 수행할 수 있습니다.
예를 들어, “자동차” 객체(Object)는 달리기 또는 멈추기 같은 메소드(Methods)를 가질 수 있습니다.
Java에서 메소드(Methods)는 함수로 정의됩니다.
class Car {
private String color;
private int speed;
public Car(String color, int speed) {
this.color = color;
this.speed = speed;
}
public void run() {
System.out.println("The car is running at " + this.speed + " km/h"); // 메소드(Methods)
}
}
3️⃣ 클래스(Class)
객체(Object)를 생성하기 위한 청사진 또는 설계도입니다.
클래스는 객체의 속성(Attributes)과 메소드(Methods)를 정의하며, 객체(Object)는 이 클래스(Class)를 기반으로 생성됩니다.
예를 들어, “Car”라는 클래스(Class)는 자동차의 속성(색상, 속도)과 행동(달리기, 멈추기)을 정의하고, 이 클래스(Class)를 사용해 다양한 “자동차” 객체(Object)를 만들 수 있습니다.
Java에서 클래스는 다음과 같이 정의됩니다.
class Car {
private String color;
private int speed;
public Car(String color, int speed) {
this.color = color;
this.speed = speed;
}
public void run() {
System.out.println("The car is running at " + this.speed + " km/h");
}
}
4️⃣ 인스턴스(Instance)
클래스(Class)로부터 생성된 실제 객체(Object)를 의미합니다.
하나의 클래스(Class)는 여러 개의 인스턴스(Instance)를 가질 수 있으며, 각 인스턴스는 고유한 속성(Attributes) 값을 가질 수 있습니다.
예를 들어,”Car” 클래스에서 “red_car”라는 인스턴스(Instance)를 생성할 수 있습니다.
Car redCar = new Car("red", 120);
redCar.run(); // "The car is running at 120 km/h" 출력
2️⃣ 객체의 특성.
1️⃣ 캡슐화(Encapsulation)
객체(Object)는 자신의 데이터를 외부로부터 은닉하고, 해당 데이터를 조작하는 메소드(Methods)를 통해서만 접근을 허용하는 특성을 가집니다.
이를 통해 데이터의 무결성을 유지할 수 있습니다.
캡슐화(Encapsulation)는 객체(Object) 내부 구현 세부 사항을 외부에서 알 필요 없이 인터페이스(Interface)만을 통해 상호작용할 수 있도록 합니다.
2️⃣ 추상화(Abstraction)
객체(Object)는 현실 세계의 사물이나 개념을 추상화하여 나타냅니다.
복잡한 시스템을 단순화하여 중요한 정보만을 표현하고, 불필요한 세부 사항을 숨깁니다.
예를 들어, 자동차의 복잡한 엔진 내부 구조는 숨기고, 사용자는 단순히 “달리기” 메소드(Methods)로 자동차를 움직일 수 있습니다.
3️⃣ 상속(Inheritance)
객체 지향 프로그래밍에서 상속(Inheritance)은 한 클래스가 다른 클래스의 속성(Attributes)과 메소드(Methods)를 물려받는 것을 의미합니다.
이를 통해 기존 클래스의 기능을 확장하거나 수정하여 새로운 클래스를 정의할 수 있습니다.
예를 들어, “Car” 클래스는 “ElectricCar”라는 하위 클래스로 확장될 수 있습니다.
class ElectricCar extends Car {
private int batteryCapacity;
public ElectricCar(String color, int speed, int batteryCapacity) {
super(color, speed);
this.batteryCapacity = batteryCapacity;
}
}
4️⃣ 다형성(Polymorphism)
다형성(Polymorphism)은 객체(Object)가 같은 인터페이스(Interface)를 사용하여 다양한 방식으로 동작할 수 있는 특성입니다.
즉, 동일한 메소드(Methods)가 다양한 클래스에서 다르게 구현될 수 있습니다.
예를 들어, “Animal”이라는 클래스에 “speak()”라는 메소드가 있다면, 이를 상속받는 “Dog”와 “Cat” 클래스는 각각의 방식으로 이 메소드를 다르게 구현할 수 있습니다.
class Animal {
public void speak() {
// 메소드 구현 생략
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow");
}
}
3️⃣ 객체의 예시.
1️⃣ 자동차 객체 예시.
class Car {
private String color;
private int speed;
public Car(String color, int speed) {
this.color = color;
this.speed = speed;
}
public void run() {
System.out.println("The " + this.color + " car is running at " + this.speed + " km/h");
}
public class Main {
public static void main(String[] args) {
// 인스턴스 생성
Car myCar = new Car("red", 100);
myCar.run(); // "The red car is running at 100 km/h" 출력
}
}
}
2️⃣ 은행 계좌 객체 예시.
class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public void deposit(double amount) {
balance += amount;
System.out.println(amount + " deposited. Ned balance: " + balance);
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
System.out.println(amount + " withdrawn. Remaining balance: " + balance);
} else {
System.out.println("Insufficient balance");
}
}
}
public class Main {
public static void main(String[] args) {
// 인스턴스 생성.
BankAccount myAccount = new BankAccount("123-456", 5000);
myAccount.deposit(1000); // "1000 deposited. New balance: 6000" 출력
myAccount.withdraw(2000); // "2000 withdraw Remaining balance: 4000" 출력
}
}
4️⃣ 결론.
객체(Object)는 클래스(Class)에 정의된 속성(Attributes)과 메소드(Methods)를 가진 독립적인 개체로, 객체 지향 프로그래밍의 핵심 구성 요소입니다.
객체(Object)는 현실 세계의 개념이나 시스템의 구성 요소를 프로그래밍적으로 표현하며, 상태(속성)와 행위(메소드)를 가지고 있습니다.
객체는 추상화, 캡슐화, 상속, 다형성과 같은 객체 지향 원칙을 따르며, 재사용 가능성과 유지보수성을 향상 시킵니다.
-
🌐[Network] TCP(Transmission Control Protocol)란 무엇일까?
🌐[Network] TCP(Transmission Control Protocol)란 무엇일까?
TCP(Transmisson Control Protocol)는 인터넷 프로토콜 스위트(Internet Protol Suite)의 핵심 프로토콜 중 하나로, 신뢰성 있는 데이터 전송을 보장하는 연결 지향형 프로토콜입니다.
TCP는 데이터 패킷을 손실 없이, 순서대로, 정확하게 전달하는 것을 목표로 하며, 인터넷을 포함한 대부분의 네트워크에서 사용됩니다.
TCP/IP라고도 불리며, IP(Internet Protocol)와 함께 동작하기 때문입니다.
1️⃣ TCP의 주요 특징.
1️⃣ 연결 지향형 프로토콜.
TCP는 데이터를 전송하기 전에 송신자와 수신자 간에 연결을 설정합니다.
이 연결을 통해 두 장치가 데이터를 주고받을 수 있는 가상 회선을 형성합니다.
연결이 설정되면, 데이터가 손실되지 않고 순서대로 전달 되도록 보장합니다.
2️⃣ 신뢰성 있는 데이터 전송.
TCP는 패킷 손실이 발생할 경우 손실된 패킷을 재전송하고, 수신한 패킷이 올바른지 확인하는 오류 검출 및 수정 메커니즘을 가지고 있습니다.
이로 인해 데이터가 손실되거나 손상되지 않고, 정확히 목적지에 도달하도록 합니다.
3️⃣ 데이터 흐름 제어.
TCP는 수신 측이 데이터를 처리할 수 있는 속도에 맞춰 데이터를 전송하는 흐름 제어 기능을 제공합니다.
이를 통해 송신 측에서 데이터를 너무 빨리 보내 수신 측이 처리하지 못하는 상황을 방지합니다.
4️⃣ 혼잡 제어.
네트워크에서 혼잡이 발생할 경우, TCP는 전송 속도를 줄여 네트워크 부하를 완화하는 혼잡 제어 기능을 수행합니다.
이를 통해 네트워크가 과부하 상태에 빠지지 않도록 보호합니다.
5️⃣ 패킷 순서 보장.
네트워크에서는 패킷이 서로 다른 경로를 통해 목적지에 도달할 수 있기 때문에, 도착 순서가 달라질 수 있습니다.
TCP(Transmission Control Protocol)는 이러한 패킷을 올바른 순서로 재조립하여 원래의 데이터로 복원합니다.
6️⃣ 패킷 오류 검출 및 수정.
TCP(Transmission Control Protocol) 패킷에 체크섬(Checksum)이라는 오류 검출 값을 포함하여, 데이터가 손상되지 않았는지 확인합니다.
만약 패킷이 손상되었거나 도착하지 않았다면, 해당 패킷을 다시 전송합니다.
2️⃣ TCP의 동작 원리.
TCP는 데이터를 송신자에서 수신자로 안전하게 전송하기 위해 세 가지 주요 단계를 거칩니다.
1️⃣ 연결 설정(3-Way Handshake)
TCP(Transmisson Control Protocol)는 데이터 전송을 시작하기 전에 송신자와 수신자 간에 3단계 핸드셰이크(3-Way Handshake)를 통해 연결을 설정합니다.
1️⃣ SYN : 송신자는 수신자에게 연결 요청(SYN)을 보냅니다.
2️⃣ SYN-ACK : 수신자는 송신자의 요청을 수락하고, 연결을 승인하는 응답(SYN-ACK)을 보냅니다.
3️⃣ ACK : 송신자는 수신자의 응답을 확인(ACK)하고, 데이터 전송을 시작할 준비가 완료됩니다.
이 과정이 완료되면 송신자와 수신자는 데이터 전송을 위한 가상 연결을 확립하게 됩니다.
2️⃣ 데이터 전송
데이터를 작은 패킷으로 나누어 전송합니다.
각 패킷은 순서 번호가 부여되며, 수신자는 이 번호를 이용해 패킷이 올바른 순서로 도착했는지 확인하고 재조립합니다.
각 패킷이 성공적으로 도착하면 수신자는 송신자에게 ACK(Acknowledgement, 확인 응답)를 보냅니다.
만약 수신자가 패킷을 받지 못하거나 패킷이 손상된 경우, 송신자는 해당 패킷을 재전송합니다.
3️⃣ 연결 해제(4-Way Handshake)
데이터 전송이 완료되면, 송신자와 수신자는 4단계 핸드 셰이크(4-Way Handshake) 과정을 통해 연결을 해제합니다.
1️⃣ FIN : 송신자는 연결을 종료하기 위한 요청(FIN)을 보냅니다.
2️⃣ ACK : 수신자는 이 요청을 수락하고 ACK(Acknowledgement, 확인 응답)을 보냅니다.
3️⃣ FIN : 수신자도 연결을 종료하고 싶을 때 FIN을 보냅니다.
4️⃣ ACK : 송신자는 수신자의 FIN에 대한 ACK(Acknowledgement, 확인 응답)를 보냅니다.
이 과정이 완료되면 송신자와 수신자 간의 연결이 종료됩니다.
3️⃣ TCP와 IP의 관계.
TCP(Transmisson Control Protocol)는 전송 계층(Transport Layer)에서 동작하는 프로토콜이며, IP(Internet Protocol)는 네트워크 계층(Network Layer)에서 동작하는 프로토콜입니다.
이 둘은 TCP/IP라는 이름으로 함께 사용되며, TCP(Transmisson Control Protocol)는 데이터 전송의 신뢰성을 보장하고, IP(Internet Protocol)는 신뢰성이나 순서 보장을 하지 않기 때문에, TCP(Transmisson Control Protocol)가 그 위에서 패킷의 순서 및 무결성을 보장하는 역할을 합니다.
4️⃣ TCP의 장점.
1️⃣ 신뢰성 : TCP(Transmisson Control Protocol)는 데이터 손실, 패킷 오류, 순서 오류 등을 자동으로 처리하여, 데이터를 정확하게 전송합니다.
2️⃣ 순서 보장 : TCP는 패킷이 순서대로 도착하도록 보장하여, 전송된 데이터가 정확하게 재조립될 수 있도록 합니다.
3️⃣ 흐름 제어 : 수신 측의 처리 속도에 맞추어 데이터를 조절함으로써, 데이터 과부하를 방지합니다.
4️⃣ 혼잡 제어 : 네트워크 혼잡 상태를 관리하여, 네트워크 부하가 발생했을 때도 효율적으로 데이터 전송을 유지합니다.
5️⃣ TCP의 단점.
1️⃣ 속도
TCP(Transmisson Control Protocol)는 데이터의 신뢰성을 보장하기 위해 많은 오버헤드가 발생합니다.
확인 응답(ACK, Acknowledgement)을 주고받고, 재전송을 수행해야 하기 때문에 UDP(User Datagram Protocol)에 비해 속도가 느릴 수 있습니다.
2️⃣ 복잡성
TCP(Transmisson Control Protocol)는 상태를 유지하고, 각 패킷의 순서와 무결성을 추절해야 하기 때문에 구현이 복잡합니다.
3️⃣ 실시간 데이터 전송에 부적합
TCP(Transmisson Control Protocol)는 실시간으로 빠르게 전송해야 하는 스트리밍 서비스나 온라인 게임 등에서는 적합하지 않습니다.
실시간 응답보다는 신뢰성이 더 중요할 때 TCP가 사용됩니다.
6️⃣ TCP와 UDP의 비교.
특징
TCP(Transmisson Control Protocol)
UDP(User Datagram Protocol)
연결 방식
연결 지향형(3-Way Handshake)
비연결형
데이터 전송 보장
신뢰성 보장, 패킷 재정송, 순서 보장
신뢰성 없음, 패킷 손실 발생 가능성
속도
상대적으로 느림
상대적으로 빠름
오류 처리
오류 검출 및 재전송
오류 검출 없음
적합한 용도
웹 브라우징, 파일 전송, 이메일 등
실시간 스트리밍, 온라인 게임 등
7️⃣ TCP의 사용 사례.
웹 브라우징(HTTP/HTTPS)
TCP는 HTTP와 HTTPS에서 사용되며, 웹 페이지가 손상 없이 정확하게 로드 될 수 있도록 보장합니다.
파일 전송(FTP, File Transport Protocol)
FTP(File Transport Protocol)는 TCP를 사용하여 파일을 신뢰성 있게 전송합니다.
이메일(SMTP/IMAP/POP3)
이메일 전송 및 수신 시, SMTP, IMAP, POP3는 TCP(Transmission Control Protocol)를 사용하여 데이터가 손실되지 않고 도착하도록 보장합니다.
원격 접속(SSH, Telent)
원격 서버에 안전하게 연결하여 명령을 전송할 때 TCP를 사용하여 신뢰성 있는 통신을 보장합니다.
8️⃣ 결론.
TCP(Transmisson Control Protocol)는 신뢰성 있는 데이터 전송을 보장하는 프로토콜로, 인터넷에서 널리 사용됩니다.
데이터가 정확하게, 손실 없이 전달되는 것을 보장하며, 연결을 설정한 후 데이터를 송수신하고, 전송된 데이터가 손상되지 않았는지 확인하며, 손상된 데이터는 재전송합니다.
웹 브라우징, 파일 전송, 이메일 전송등 신뢰성이 중요한 애플리케이션에서 주로 사용되며, 이로 인해 네트워크의 안정성을 크게 향상시킵니다.
-
🌐[Network] 네트워크 포트(Network Port)란 무엇일까?
🌐[Network] 네트워크 포트(Network Port)란 무엇일까?
네트워크 포트(Network Port)는 컴퓨터나 네트워크 장치가 서로 통신할 때, 특정 프로세스나 서비스에 대한 데이터의 출입을 식별하기 위한 논리적인 통신 경로입니다.
포트는 IP 주소와 함께 사용되어, 어떤 애플리케이션이 데이터를 송수신을 해야 하는지를 컴퓨터나 네트워크 장치가 알 수 있도록 도와줍니다.
네트워크에서, IP 주소는 장치 자체를 식별하는 역할을 하며, 포트 번호는 그 장치 내에서 특정 애플리케이션이나 서비스를 식별하는 역할을 합니다.
포트는 정수 값으로 표현되며, 보통 0부터 65535까지의 숫자를 가집니다.
🙋♂️ 네트워크, 포트, 도메인 이름, IP, DNS
1️⃣ 네트워크 포트의 주요 역할.
1️⃣ 데이터 전달 경로 지정.
IP 주소는 네트워크 상에서 장치를 식별하지만, 같은 장치 내에서 여러 프로그램이 동시에 실행될 수 있기 때문에, 포트 번호는 특정 프로그램이나 서비스로 데이터를 전달하는 경로를 지정하는 역할을 합니다.
👉 예시.
예를 들어, 한 컴퓨터에서 웹 브라우저와 이메일 클라이언트가 동시에 실행 중일 때, IP 주소는 컴퓨터를 식별하고, 포트 번호는 각각의 서비스(웹 브라우저와 이메일 클라이언트)를 식별하여 데이터를 적절한 애플리케이션으로 전달합니다.
2️⃣ 프로토콜 기반 통신.
포트 번호는 네트워크 프로토콜에 따라 각기 다른 애플리케이션 또는 서비스를 구분하는 데 사용됩니다.
예를 들어, HTTP 프로토콜은 기본적으로 포트 80을 사용하고, HTTPS는 포트 443을 사용합니다.
이러한 표준 포트 번호를 통해 데이터가 정확한 프로토콜과 애플리케이션으로 전달됩니다.
2️⃣ 포트 번호의 범위.
포트 번호는 0번에서 65535번까지 사용할 수 있으며, 크게 세 가지 범위로 나눌 수 있습니다.
1️⃣ Well-Known Ports(0-1023)
0번부터 1023번까지의 포트는 널리 알려진 포트로, 주로 표준 네트워크 프로토콜이 사용하는 포트입니다.
대부분의 서비스는 고정된 포트를 사용하며, 주로 시스템 수준에서 예약된 포트입니다.
👉 예시.
포트 80 : HTTP(웹 브라우징)
포트 443 : HTTPS(보안 웹 브라우징)
포트 21 : FTP(파일 전송 프로토콜)
포트 22 : SSH(보안 쉘)
포트 25 : SMTP(이메일 전송)
2️⃣ Registered Ports (1024-49151)
1024번 부터 49151번까지의 포트는 등록된 포트로, 특정 애플리케이션이나 서비스가 사용하도록 등록된 포트입니다.
이는 공식적으로 IANA(Internet Assigned Numbers Authority)에서 관리되며, 특정 기업이나 애플리케이션에 할당될 수 있습니다.
👉 예시.
포트 3306 : MySQL 데이터베이스
포트 1433 : Microsoft SQL Server
포트 8080 : HTTP 대체 포트(웹 개발 또는 프록시 서버)
3️⃣ Dynamic/Private Ports (49152-65535)
49152번부터 65535번까지의 포트는 동적 포트 또는 임시 포트로, 클라이언트 프로그램이 동적으로 할당하여 사용하는 포트입니다.
주로 응용 프로그램이 실행될 때, 시스템이 자동으로 포트 번호를 할당하여 사용합니다.
이러한 포트는 주로 클라이언트 측에서 서버와의 통신을 위해 사용되며, 세션이 종료되면 포트 번호가 해제됩니다.
3️⃣ 포트와 IP 주소의 관계.
IP 주소는 네트워크 상에서 컴퓨터나 네트워크 장치를 식별하는 주소입니다.
포트 번호는 그 장치 내에서 어떤 애플리케이션이 통신할 것인지를 결정합니다.
예를 들어, 웹 브라우저를 사용해 http://example.com에 접속할 때, 브라우저는 포트 80(HTTP 기본 포트)으로 서버에 요청을 보냅니다.
서버는 포트 80을 통해 요청을 받아, 웹 페이지 데이터를 클라이언트에게 반환합니다.
IP 주소와 포트 번호를 결합하여 소켓을 형성합니다.
소켓은 네트워크 통신에서 IP 주소 + 포트 번호로 구성된 단위로, 이를 통해 통신이 이루어 집니다.
4️⃣ 네트워크 포트의 예.
1️⃣ HTTP와 HTTPS
포트 80은 HTTP 트래픽을 처리하는 데 사용되며, 포트 443은 HTTPS(암호화된 웹 트래픽)를 처리하는 데 사용됩니다.
예시: http://example.com (HTTP) 또는 https://example.com:443 (HTTPS)
2️⃣ SSH
포트 22는 SSH(Secure Sheel) 프로토콜을 사용하는 원격 접속 서비스에서 사용됩니다.
SSH는 네트워크 상에서 원격 시스템에 보안 연결을 설정하는 데 사용됩니다.
3️⃣ FTP
포트 21은 FTP(File Transfer Protocol)를 처리하는 데 사용됩니다.
이를 통해 서버와 클라이언트 간에 파일을 전송할 수 있습니다.
예시: ftp://example.com:21
4️⃣ 데이터베이스
포트 3306은 MySQL 데이터베이스와 연결할 때 사용되며, 클라이언트는 이 포트를 통해 MySQL 서버에 접근합니다.
포트 1433은 Microsoft SQL Server가 사용하는 포트입니다.
5️⃣ 이메일
포트 25는 SMTP(Simple Mail Transfer Protocol)를 사용하여 이메일을 전송하는 데 사용됩니다.
포트 993은 IMAP(Internet Message Access Protocol)을 통한 보안 이메일 접근에 사용됩니다.
5️⃣ 포트 스캐닝과 보안.
1️⃣ 포트 스캐닝.
포트 스캐닝은 네트워크 보안에서 중요한 활동입니다.
해커는 시스템에 열려있는 포트를 찾기 위해 포트 스캐닝을 수행하여, 취약한 포트를 악용할 수 있습니다.
시스템 보안을 강화하려면 필요하지 않은 포트는 차단하거나 방화벽을 통해 관리하는 것이 중요합니다.
2️⃣ 방화벽과 포트 차단.
방화벽(Firewall)은 특정 포트에 대한 외부 접근을 차단하거나 허용할 수 있습니다.
보안 강화를 위해, 네트워크 관리자는 불필요한 포트를 닫고, 필요한 포트만 열어두는 것이 중요합니다.
3️⃣ 포트 포워딩.
포트 포워딩은 네트워크 외부에서 들어오는 트래픽을 내부 네트워크의 특정 장치로 전달하는 과정입니다.
이를 통해 외부 네트워크와 내부 네트워크 간의 통신을 원활하게 할 수 있습니다.
6️⃣ 결론.
네트워크 포트는 네트워크 상에서 특정 어플리케이션이나 서비스에 대한 통신 경로를 제공하는 논리적인 번호입니다.
포트는 IP 주소와 함께 사용되어, 어떤 애플리케이션이 데이터를 처리해야 하는지를 결정하며, 데이터 전송의 목적지를 명확히 지정해줍니다.
포트 번호는 네트워크 프로토콜마다 고유하게 사용되며, 이를 통해 웹 브라우징, 이메일 전송, 파일 전송과 같은 다양한 작업이 이루어집니다.
포트 관리와 보안은 네트워크 운영에서 매우 중요한 부분입니다.
-
🌐[Network] IP(Internet Protocol)란 무엇일까?
🌐[Network] IP(Internet Protocol)란 무엇일까?
IP(Internet Protocol)는 컴퓨터 네트워크에서 데이터를 전송하기 위한 주소 체계이자 프로토콜입니다.
IP는 패킷(Packet) 단위로 데이터를 전송하며, 데이터를 전송할 발신자와 수신자를 식별하기 위해 고유한 IP 주소를 사용합니다.
IP는 네트워크 계층(Network Layer)에서 동작하며, 인터넷과 같은 연결망에서 장치들이 서로 통신할 수 있도록 해줍니다.
1️⃣ IP의 주요 역할
1️⃣ 주소 지정.
IP(Internet Protocol)는 네트워크 상에서 각 장치(컴퓨터, 서버, 라우터 등)에 고유한 IP 주소를 부여하여, 주고받는 송신자와 수신자를 식별합니다.
IP 주소는 각 장치가 네트워크 상에서 유일하게 구분될 수 있는 식별자로, 데이터를 정확한 목적지로 전달하는 데 사용됩니다.
2️⃣ 데이터 패킷 전송.
IP(Internet Protocol)는 데이터를 작은 패킷(Packet) 단위로 나누어 전송합니다.
이러한 패킷(Packet)들은 각각 IP 주소를 통해 발신지에서 목적지까지 최적의 경로로 전달됩니다.
패킷(Packet)은 서로 다른 경로를 통해 전달될 수 있으며, 목적지에서 다시 조립됩니다.
3️⃣ 경로 설정 및 라우팅.
IP는 네트워크 장비인 라우터를 통해 패킷(Packet)의 전송 경로를 결정합니다.
라우터는 목적지 IP 주소를 확인하고, 패킷(Packet)이 올바른 경로로 전달되도록 합니다.
IP(Internet Protocol)는 패킷(Packet)이 네트워크를 목적지까지 효율적으로 전달되도록 경로를 설정하는 역할을 합니다.
2️⃣ IP 주소의 구조.
IP(Internet Protocol) 주소는 네트워크 상에서 장치를 구분하는 고유한 숫지 식별자입니다.
IP(Internet Protocol) 주소는 IPv4와 IPv6의 두 가지 버전으로 나뉩니다.
1️⃣ IPv4(Internet Protocol version 4)
32비트로 구성된 주소로, 닷(deciaml) 표기법을 사용하여 4개의 숫자로 표현됩니다.
각 숫자는 0~255 범위 내에 있으며 점(.)으로 구분됩니다.
예시: 192.169.1.1
IPv4는 약 43억 개의 고유 주소를 제공하지만, 인터넷 사용이 폭발적으로 증가하면서 주소가 고갈되고 있어, 이를 대체하기 위해 IPv6가 도입되었습니다.
2️⃣ IPv6(Internet Protocol version 6)
128비트로 구성된 주소로, 16진수를 사용해 8개의 그룹으로 표현됩니다.
각 그룹은 콜론(,)으로 구분됩니다.
예시: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6는 매우 많은(340언델리온) 주소 공간을 제공하여, 미래의 인터넷 사용을 충분히 지원할 수 있습니다.
3️⃣ IP의 주요 기능.
1️⃣ 데이터 패킷화 및 전송.
IP는 데이터를 패킷(Packet) 단위로 분할아혀 전송합니다.
각 패킷에는 발신자 IP(Internet Protocol) 주소와 수신자 IP(Internet Protocol) 주소가 포함되며, 패킷은 네트워크 상의 여러 장비(라우터 등)를 거쳐 목적지에 도달합니다.
2️⃣ 최적 경로 설정.
IP는 네트워크 내에서 최적의 경로를 통해 데이터를 전달합니다.
패킷(Packet)은 각 경로를 지나며, IP(Internet Protocol) 주소를 기반으로 라우팅됩니다.
이를 통해 데이터가 가장 빠르고 효율적으로 전달될 수 있습니다.
3️⃣ 비연결성.
IP는 비연결성 프로토콜입니다.
즉, 데이터를 전송할 때 송신자와 수신자 간에 사전 연결 설정을 하지 않습니다.
각 패킷(Packet)은 독립적으로 전송되며, 네트워크 상황에 따라 서로 다른 경로로 전달될 수 있습니다.
4️⃣ 무결성 보장 없음.
IP(Internet Protocol)는 데이터를 전송할 때 패킷(Packet)의 신뢰성이나 순서 보장을 하지 않습니다.
패킷(Packet)이 손실되거나, 순서가 뒤바뀔 수 있으며, 이는 상위 계층의 프로토콜(TCP, Transmission Control Protocol)이 처리합니다.
IP(Internet Protocol)는 단지 데이터를 전송하는 역할을 수행합니다.
🙋♂️ TCP(Transmission Control Protocol)란 무엇일까요
4️⃣ IP의 동작 방식.
1️⃣ 패킷 생성.
송신 장치는 데이터를 작은 패킷(Packet)으로 나누고, 각 패킷에 IP 헤더를 추가합니다.
이 헤더에는 송신자와 수신자의 IP(Internet Protocol) 주소가 포함됩니다.
2️⃣ 라우팅.
패킷(Packet)이 네트워크를 통해 전송될 때, 각 패킷(Packet)은 라우터(Router)를 거쳐 최종 목적지로 향합니다.
라우터는 패킷(Packet)의 목적지 IP 주소를 분석하여, 패킷을 적절한 경로로 전송합니다.
3️⃣ 패킷 수신.
목적지 장치에서 패킷(Packet)을 수신하면, 패킷(Packet)을 다시 원래의 데이터로 재조립합니다.
상위 계층의 프로토콜(TCP(Transmission Control Protocol) / UDP(User Datagram Protocol))은 패킷의 손신 여부나 순서를 확인하고, 데이터를 정확하게 전달하도록 보장합니다.
5️⃣ IP의 종류.
1️⃣ 공인 IP 주소(Public IP Address)
공인 IP 주소(Public IP Address)는 인터넷 상에서 고유한 주소로, 인터넷 서비스 제공업체(ISP, Internet Service Provider)에 의해 할당됩니다.
이 주소는 전 세계에서 유일하며, 인터넷을 통해 다른 장치들과 통신할 수 있습니다.
2️⃣ 사설 IP 주소(Private IP Address)
사설 IP 주소(Private IP Address)는 로컬 네트워크에서 사용되는 IP(Internet Protocol) 주소 입니다.
같은 사설 IP 주소는 여러 네트워크에서 중복해서 사용할 수 있지만, 인터넷과 직접 통신할 수 없습니다.
사설 IP는 NAT(Network Address Translation)을 통해 공인 IP(Public IP)로 변환되어 인터넷에 접근할 수 있습니다.
👉 사설 IP 주소 대역
1.0.0.0 ~ 10.255.255.255
172.16.0.0 ~ 172.31.255.255
192.168.0.0 ~ 192.168.255.255
6️⃣ IP 프로토콜의 버전.
1️⃣ IPv4(Internet Protocol version 4)
IPv4는 인터넷이 상용화된 초기부터 사용된 프로토콜로, 32비트 주소 체계를 사용합니다.
43억 개의 IP 주소를 제공하지만, 인터넷 장치의 폭발적 증가로 인해 IP 주소가 부족해지고 있습니다.
2️⃣ IPv6(Internet Protocol version 6)
IPv6는 128비트 주소 체계를 사용하여 거대한 주소 공간을 제공합니다.
IPv6는 IPv4의 주소 부족 문제를 해결하기 위해 도입되었으며, 주소 자동 구성 및 향상된 보안 기능을 지원합니다.
7️⃣ IP 프로토콜의 역할과 TCP/IP 모델.
IP는 네트워크 계층(Network Layer)에서 동작하는 프로토콜로, 데이터가 목적지에 도달할 수 있도록 경로 설정과 주소 지정을 처리합니다.
IP는 TCP와 함께 동장하여 데이터를 안전하게 전송하는 TCP/IP 모델의 일부입니다.
TCP(Transmission Control Protocol)는 전송 계층에서 데이터를 신뢰성 있게 전송하며, IP(Internet Protocol)는 네트워크 계층에서 패킷(Packet)을 올바른 경로로 전송합니다.
🙋♂️ OSI 7계층 모델
8️⃣ IP와 관련된 주요 프로토콜.
1️⃣ TCP(Transmission Control Protocol)
TCP(Transmission Control Protocol)는 신뢰성 있는 데이터 전송을 보장하는 프로토콜입니다.
IP(Internet Protocol)는 패킷(Packet)을 목적지로 전달하는 역할을 하지만, TCP(Transmission Control Protocol)는 패킷의 무결성, 순서 보장, 오류 검출 및 수정을 담당합니다.
2️⃣ UDP(User Datagram Protocol)
UDP(User Datagram Protocol)는 비연결형 프로토콜로, IP(Internet Protocol)와 마찬가지로 데이터를 전송하지만, 신뢰성을 보장하지 않습니다.
TCP(Transmission Control Protocol)와 달리 오버헤드(Overhead)가 적고 빠르기 때문에, 실시간 스트리밍이나 온라인 게임에서 많이 사용됩니다.
3️⃣ ICMP(Internet Control Message Protocol)
ICMP(Internet Control Message Protocol)는 네트워크 진단 및 오류 처리를 위한 프로토콜입니다.
Ping 명령어는 ICMP를 사용하여 네트워크 연결 상태를 확인합니다.
9️⃣ IP의 사용 사례.
인터넷 통신
IP는 인터넷 상의 모든 장치가 서로 통신할 수 있도록 주소를 제공하고, 데이터를 전송하는 핵심 프로토콜입니다.
로컬 네트워크(LAN, Local Area Network, 근거리 통신망)
IP는 가정이나 사무실에서 로컬 네트워크 내 장치들이 서로 데이터를 주고받을 수 있게 합니다.
네트워크 장비 간 통신
라우터, 스위치, 서버 등 네트워크 장비들이 IP 주소를 통해 서로 통신하고 데이터를 전달합니다.
1️⃣0️⃣ 결론.
IP(Internet Protocol)는 네트워크 상의 장치들이 데이터를 주고받을 수 있도록 하는 기본 프로토콜입니다.
IP 주소를 통해 각 장치를 고유하게 식별하고, 데이터를 패킷(Packet) 단위로 나누어 전달합니다.
IPv4와 IPv6 두 가지 버전이 있으며, 경로 설정, 주소 지정, 데이터 패킷화 등을 담당합니다.
IP는 네트워크 통신의 핵심적인 역할을 수행하며, 인터넷과 로컬 네트워크에서 모든 장치 간 통신을 가능하게 합니다.
-
🌐[Network] LAN 내에서 전달되는 데이터 프레임이란 무엇일까?
🌐[Network] LAN 내에서 전달되는 데이터 프레임이란 무엇일까?
LAN(Local Area Network, 근거리 통신망) 내에서 전달되는 데이터 프레임(Data Frame)은 네트워크 계층의 데이터가 데이터 링크 계층(OSI 7계층의 2계층)에서 전송 단위로 사용되는 구조입니다.
프레임(Frame)은 이더넷(Ethernet)과 같은 LAN(Local Area Network, 근거리 통신망) 기술에서 데이터 패킷(Data Packet)을 네트워크 장치 간에 전송하는 데 사용되며, 헤더, 데이터(페이로드,Payload), 트레일러로 구성됩니다.
🙋♂️ 이더넷(Ethernet)이란 무엇일까요?
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
🙋♂️ OSI 7계층 모델
1️⃣ 데이터 프레임의 구성 요소.
1️⃣ 헤더(Header)
목적지 MAC 주소
데이터가 어느 장치로 전송될지 나타냅니다.
LAN(Local Area Network, 근거리 통신망) 내에서 연결된 네트워크 장치들(예: 컴퓨터, 프린터, 스위치 등)은 MAC 주소를 통해 식별됩니다.
🙋♂️ MAC(Media Access Control) 주소란 무엇일까요?
2️⃣ 데이터(페이로드, Payload)
상위 계층에서 전송된 실제 데이터가 포함됩니다.
주로 네트워크 계층(예: IP 패킷)의 데이터를 전달하며, 최대 1500바이트의 데이터를 포함할 수 있습니다.
3️⃣ 트레일러(Trailer)
프레임 체크 시퀀스(FCS, Frame Check Sequence)
데이터 전송 중 오류를 감지하는 오류 검사 코드를 포함합니다.
수신 측에서 데이터 프레임이 손상되지 않았는지 확인하는 데 사용합니다.
2️⃣ 데이터 프레임의 동작 원리.
1️⃣ 출발지 장치에서 생성.
데이터 링크 계층에서 상위 계층의 데이터를 받아 프레임으로 변환합니다.
출발지 MAC 주소와 목적지 MAC 주소가 프레임에 추가됩니다.
2️⃣ LAN 내에서 전송.
이더넷 케이블, 스위치 또는 무선 엑세스 포인트(WAP, Wireless Access Point)를 통해 프레임이 LAN(Local Area Network, 근거리 통신망) 내에서 전송됩니다.
스위치(Switch)는 MAC 주소 테이블을 사용하여 목적지 MAC 주소를 분석한 후, 해당 장치로 프레임을 전달합니다.
3️⃣ 수신 측에서 재조립
목적지 장치는 해당 프레임을 수신하고, 오류가 없는지 검사한 후 데이터 링크 계층의 헤더와 트레일러를 제거하고 상위 계층(네트워크 계층)으로 데이터를 전달합니다.
3️⃣ 데이터 프레임의 역할
1️⃣ 데이터 전달의 기본 단위.
LAN(Local Area Network, 근거리 통신망) 내에서 데이터 통신의 기본 단위로 사용됩니다.
이더넷(Ethernet), Wi-Fi 등 다양한 LAN 기술에서 데이터를 전송할 때 프레임이 사용됩니다.
2️⃣ MAC(Media Access Control)주소를 통한 장치 식별.
MAC(Media Access Control) 주소를 사용하여 LAN(Local Access Network, 근거리 통신망) 내에서 장치를 식별하고, 데이터 프레임을 해당 장치로 전달합니다.
3️⃣ 에러 감지.
프레임에 포함된 FCS(Frame Check Sequence)를 통해 데이터 전송 중 오류 감지를 수행합니다.
오류가 발생한 경우 데이터 프레임은 폐기되며, 필요시 재전송됩니다.
4️⃣ 데이터 프레임 전송 과정 예시.
1️⃣ 컴퓨터 A에서 컴퓨터 B로 데이터를 전송하려면, 컴퓨터 A는 컴퓨터 B의 MAC 주소를 사용하여 데이터 프레임을 생성합니다.
2️⃣ 이 프레임은 스위치 등 네트워크 장치를 거쳐 컴퓨터 B로 전달됩니다.
3️⃣ 컴퓨터 B는 자신의 MAC(Media Access Control) 주소와 일치하는 프레임을 수신하고, 해당 데이터를 처리합니다.
5️⃣ 결론.
LAN(Local Area Network, 근거리 통신망) 내에서 데이터 프레임은 네트워크에서 데이터를 전송하기 위한 기본 단위입니다.
프레임은 데이터 링크 계층에서 MAC(Media Access Control) 주소를 사용하여 출발지와 목적지 장치를 식별하며, 데이터를 전송하는 동안 오류 감지 기능을 수행합니다.
이는 이더넷(Ethernet) 기반 네트워크에서 데이터를 신뢰성 있게 전송하는 핵심 요소입니다.
-
💾 [CS] ORM이란 무엇일까요?
💾 [CS] ORM이란 무엇일까요?
ORM(Object-Relational Mapping)은 객체-관계 매핑을 의미하며, 객체 지향 프로그래밍 언어(예: Java, Python 등)에서 사용하는 객체와 관계형 데이터베이스의 테이블 간의 데이터를 매핑하는 기술을 말합니다.
ORM(Object-Relational Mapping)은 객체 지향 방식과 관계형 데이터베이스의 데이터 구조가 서로 다르다는 문제를 해결하기 위한 솔루션으로, 프로그래머가 데이터베이스의 세부적인 SQL 쿼리 없이 객체 모델을 통해 데이터베이스와 상호작용할 수 있도록 도와줍니다.
1️⃣ ORM(Object-Relational Mapping)의 주요 기능.
1️⃣ 객체와 테이블 간의 매핑.
객체 지향 언어에서는 데이터가 객체로 표현되고, 관계형 데이터베이스에서는 데이터가 테이블 형태로 저장됩니다.
ORM(Object-Relational Mapping)은 프로그래밍 언어의 객체와 데이터베이스의 테이블을 자동으로 매핑하여, 객체를 이용해 데이터베이스와 상호작용할 수 있도록 합니다.
2️⃣ SQL 추상화.
ORM(Object-Relational Mapping)은 SQL 쿼리를 자동으로 생성하고, 프로그래머가 객체를 사용하여 데이터를 조회, 삽입, 삭제, 수정하는 코드를 작성할 수 있게합니다.
이를 통해 SQL 없이도 데이터베이스 작업을 쉽게 수행할 수 있습니다.
🙋♂️ SQL이란?
🙋♂️ 데이터베이스란?
3️⃣ 데이터베이스 독립성.
ORM(Object-Relational Mapping)은 특정 데이터베이스에 종속되지 않고, 다양한 데이터베이스에서 동일한 코드로 동작할 수 있습니다.
데이터베이스를 변경하더라도 ORM을 사용하면 코드를 거의 수정하지 않아도 됩니다.
4️⃣ 객체 모델 중심의 개발.
ORM을 사용하면 객체 모델을 통해 데이터베이스와 상호작용하기 때문에, 데이터베이스와의 상호작용이 객체 지향 프로그래밍의 방식과 일관성을 유지합니다.
이는 비즈니스 로직과 데이터베이스 상호작용을 자연스럽게 통합하는 데 도움을 줍니다.
🙋♂️ 비즈니스 로직(Business Logic)이란?
🙋♂️ API 설계, 계층형 아키텍처, 트랜잭션, 엔티티(Entity), 비즈니스 로직과 비즈니스 규칙의 차이점.
2️⃣ ORM의 동작 원리.
1️⃣ 객체와 데이터베이스 테이블 매핑.
객체의 속성은 데이터베이스 테이블의 컬럼(Column, 열)에 대응하고, 객체의 인스턴스는 테이블의 로우(Row, 행)에 대응됩니다.
예를 들어, 객체 모델에 User 클래스가 있다면, 데이터베이스에는 User 테이블이 있고, 그 속성 id, name, email 등은 테이블의 컬럼(Column, 열)과 매핑됩니다.
2️⃣ SQL 자동 생성.
ORM(Object-Relational Mapping) 라이브러리는 객체를 기반으로 SELECT, INSERT, UPDATE, DELETE와 같은 SQL 쿼리를 자동으로 생성합니다.
예를 들어, User 객체를 데이터베이스에 저장하는 코드를 작성하면, ORM(Object-Relational Mapping)이 자동으로 INSERT SQL 쿼리를 생성하여 테이블에 해당 데이터를 삽입합니다.
3️⃣ 데이터베이스 연동.
ORM(Object-Relational Mapping)은 객체 상태를 추적하고, 변경 사항이 있을 경우 이를 데이터베이스와 동기화합니다.
객체의 속성값이 변경되면, ORM(Object-Relationl Mapping)은 자동으로 UPDATE 쿼리를 생성하고 실행하여 데이터베이스를 업데이트합니다.
3️⃣ ORM의 장점.
1️⃣ 생산성 향상.
ORM(Object-Relational Mapping)을 사용하면 SQL 작성을 줄이고, 객체 지향 프로그래밍 방식으로 데이터를 처리할 수 있기 때문에, 개발자는 더 적은 코드로 데이터베이스와 상호작용할 수 있습니다.
이는 개발 속도를 높이고 유지보수를 쉽게합니다.
2️⃣ 데이터베이스 독립성.
ORM(Object-Relational Mapping)은 특정 데이터베이스에 의존하지 않으며, 데이터베이스를 변경하더라도 ORM(Object-Relational Mapping) 라이브러리만 맞추면 프로그램 코드를 거의 수정하지 않고도 다양한 데이터베이스에서 동작할 수 있습니다.
3️⃣ 보안성
ORM(Object-Relational Mapping)은 자동으로 SQL 쿼리를 생성하기 때문에 SQL 인젝션 공격과 같은 보안 취약점을 방지하는 데 도움이 됩니다.
직접 SQL을 작성할 필요가 줄어들기 때문에, 보안성이 향상됩니다.
4️⃣ 유지보수성.
객체 지향 설계를 유지하면서 데이터베이스와 상호작용할 수 있어, 코드의 가독성과 유지보수가 쉬워집니다.
데이터베이스 관련 변경이 필요할 때도 객체 모델을 통해 쉽게 변경할 수 있습니다.
4️⃣ ORM의 단점.
1️⃣ 복잡한 쿼리 작성의 한계.
ORM은 복잡한 쿼리 최적화나 특정한 SQL 기능을 충분히 지원하지 않을 수 있습니다.
매우 복잡한 쿼리가 필요한 경우 ORM 대신 직접 SQL을 작성해야 할 때도 있습니다.
2️⃣ 성능 이슈.
자동으로 SQL 쿼리를 생성하는 ORM은 직접 생성한 SQL에 비해 성능이 다소 떨어질 수 있습니다.
대규모 트래픽이나 데이터 처리가 많은 환경에서는 ORM(Object-Relational Mapping) 사용이 비효율적일 수 있습니다.
3️⃣ 추상화로 인한 제어력 감소.
ORM(Object-Relational Mapping)은 SQL을 추상화하기 때문에, 데이터베이스의 세부적인 제어가 어렵습니다.
SQL의 세부 동작을 직접 관리하고 싶을 때는 ORM(Object-Relational Mapping)보다 직접 SQL 작성이 더 나을 수 있습니다.
🙋♂️ 추상화(abstraction)
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’이란 무엇일까?
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.
5️⃣ 대표적인 ORM 라이브러리.
Hibernate
Java와 JPA(Java Persistence API)를 지원하는 가장 널리 사용되는 ORM 프레임워크입니다.
Hibernate는 객체와 관계형 데이터베이스 간의 매핑을 자동으로 처리하며, 다양한 데이터베이스를 지원합니다.
🙋♂️ JPA란 무엇인가요?
🙋♂️ JPA를 사용하는 이유.
6️⃣ 결론.
ORM(Object-Relational Mapping)은 객체 지향 프로그래밍에서 객체와 관계형 데이터베이스 간의 매핑을 관리하는 기술로, 개발자가 객체를 사용하여 데이터베이스 작업을 수행할 수 있게 해줍니다.
이를 통해 개발자는 SQL 작성의 번거로움을 줄이고, 생성성, 유지보수성, 보안성을 높일 수 있습니다.
그러나 복잡한 쿼리 처리나 성능 문제에 있어서는 SQL을 장성하는 것이 더 나을 수 있습니다.
ORM(Object-Relation Mapping)을 적절히 활용하면 코드의 가독성과 효율성을 크게 향상시킬 수 있습니다.
-
🌐[Network] WAN(Wide Area Network)이란 무엇일까요?
🌐[Network] WAN(Wide Area Network)이란 무엇일까요?
WAN(Wide Area Network, 광역 통신망)은 지리적으로 멀리 떨어진 지역들을 연결하는 네트워크입니다.
WAN(Wide Area Network, 광역 통신망)은 도시, 국가, 심지어 전 세계에 걸쳐있는 네트워크를 연결할 수 있으며, 일반적으로 여러 LAN(Local Area Network, 근거리 통신망)이나 MAN(Metropolitan Area Network, 도시권 통신망)을 서로 연결하여 데이터를 주고받을 수 있게 해줍니다.
인터넷이 가장 대표적인 WAN의 예입니다.
🙋♂️LAN(Local Area Network)이란 무엇일까요?
1️⃣ WAN(Wide Area Network, 광역 통신망)의 주요 특징.
1️⃣ 광범위한 지리적 범위.
WAN(Wide Area Network, 광역 통신망)은 멀리 떨어진 지역에 있는 네트워크들을 연결합니다.
예를 들면, 한 나라의 여러 도시나, 심지어 다른 나라의 네트워크까지 연결할 수 있습니다.
WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)과 달리 국가 간, 대륙 간 네트워크를 연결할 수 있습니다.
2️⃣ 낮은 데이터 전송 속도.
WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)에 비해 상대적으로 낮은 데이터 전송 속도를 제공합니다.
네트워크 장비 간의 물리적 거리가 멀기 때문에, 데이터 전송 속도가 느려질 수 있습니다.
하지만 광케이블과 같은 고속 네트워크 기술을 사용하여 성능을 향상시킬 수 있습니다.
3️⃣ 인터넷 서비스 제공업체(ISP, Internet Service Provider)를 통한 연결.
WAN(Wide Area Network)은 일반적으로 ISP(인터넷 서비스 제공업체, Internet Service Provider)를 통해 연결됩니다.
ISP(인터넷 서비스 제공업체, Internet Service Provider)는 WAN(Wide Area Network)을 통해 여러 지역이나 국가를 연결하는 네트워크 인프라를 제공합니다.
개인이나 회사는 ISP(Internet Service Provider)의 WAN(Wide Area Network, 광역 통신망)을 사용해 인터넷을 통해 다른 네트워크와 연결됩니다.
4️⃣ 고비용.
WAN(Wide Area Network)은 구축과 유지비용이 매우 높습니다.
지리적으로 먼 거리를 연결하기 위해 복잡한 장비와 기술이 필요하며, 대규모 네트워크 인프라가 요구됩니다.
WAN(Wide Area Network, 광역 통신망) 연결을 위해 고속 광케이블, 위성 연결, 전용선등이 사용됩니다.
5️⃣ 다양한 연결 기술 사용.
WAN(Wide Area Network, 광역 통신망)은 광케이블, 위성 연결, 전용선 DSL(디지털 가입자 회선), LTE/5G와 같은 여러 기술을 사용하여 데이터를 전송합니다.
이러한 기술은 거리에 따른 속도와 성능에 영향을 미칩니다.
2️⃣ WAN의 구성 요소.
1️⃣ 라우터(Router)
라우터(Router)는 WAN(Wide Area Network, 광역 통신망)에서 데이터를 적절한 네트워크로 경로 설정하고 전송하는 역할을 합니다.
여러 LAN(Local Area Network, 근거리 통신망)을 서로 연결하거나, LAN(Local Area Network, 근거리 통신망)을 인터넷과 연결하는 데 사용됩니다.
2️⃣ 전용선(Leased Line)
전용선은 두 지점 간에 데이터를 전송하기 위해 전용으로 사용되는 통신 회선입니다.
일반적으로 기업이 본사와 지사 간의 통신을 위해 전용선으로 사용하여, 보안성과 성능을 확보할 수 있습니다.
3️⃣ 스위치(Switch)
스위치는 WAN(Wide Area Network, 광역 통신망)에서도 네트워크 장치 간의 데이터를 효율적으로 전달하기 위해 사용됩니다.
LAN(Local Area Network, 근거리 통신망)에서도 많이 사용되지만, WAN(Wide Area Network, 광역 통신망)에서는 더 큰 규모의 데이터 흐름을 관리하는 스위치가 필요합니다.
4️⃣ 방화벽(Firewall)
WAN(Wide Area Network, 광역 통신망)을 통한 네트워크 통신을 보호하기 위해 방화벽이 사용됩니다.
외부에서 들어오는 불법적인 접근이나 해킹을 차단하고, 네트워크의 보안을 강화하는 역할을 합니다.
5️⃣ 게이트웨이(Gateway)
서로 다른 네트워크 프로토콜을 사용하는 네트워크 간에 통신을 가능하게 해주는 장비입니다.
WAN(Wide Area Network, 광역 통신망)에서는 여러 종류의 네트워크를 연결할 수 있으므로, 서로 다른 네트워크 간의 통신을 위해 게이트웨이가 사용됩니다.
3️⃣ WAN의 예시.
1️⃣ 인터넷(Internet)
인터넷(Internet)은 가장 큰 WAN(Wide Area Network, 광역 통신망)의 예입니다.
전 세계의 수많은 LAN(Local Area Network, 근거리 통신망)과 WAN(Wide Area Network, 광역 통신망)이 인터넷을 통해 연결되어 통신을 주고받습니다.
인터넷은 ISP(Internet Service Provider, 인터넷 서비스 제공업체)들이 제공하는 네트워크 인프라를 통해 전 세계에서 데이터를 주고받을 수 있게 해줍니다.
2️⃣ 기업 WAN(Wide Area Network, 광역 통신망)
대기업은 본사와 여러 지사 간의 데이터를 교환하기 위해 자체적인 WAN을 구축할 수 있습니다.
예를 들어, 서울에 본사를 둔 회사가 부산, 뉴욕, 도쿄의 지사와 WAN(Wide Area Network, 광역 통신망)을 통해 연결하여 데이터를 공유하고, 협업을 진행할 수 있습니다.
3️⃣ 은행 네트워크
전 세계에 지점을 두고 있는 은행은 WAN(Wide Area Network, 광역 통신망)을 통해 각 지점 간에 데이터를 수행합니다.
고객의 계좌 정보, 거래 내역 등을 전송하고, 실시간으로 금융 데이터를 동기화합니다.
4️⃣ 정부 네트워크
여러 국가기관이 WAN(Wide Area Network, 광역 통신망)을 사용해 중앙 정부와 지방 정부 간의 데이터를 전송하거나, 다른 국가 기관과 통신하는 네트워크를 구축합니다.
4️⃣ WAN의 장점.
1️⃣ 지리적 한계를 넘는 통신.
WAN(Wide Area Network, 광역 통신망)을 사용하면 멀리 떨어진 지리적 위치에 있는 네트워크와도 쉽게 통신할 수 있습니다.
국가 간, 대륙간 연결도 WAN(Wide Area Network, 광역 통신망)을 통해 가능합니다.
2️⃣ 자원 공유.
WAN(Wide Area Network, 광역 통신망)을 통해 여러 지역에 흩어진 네트워크 사용자들이 데이터와 자원을 공유할 수 있습니다.
예를 들어, 한 회사의 여러 지사에서 파일 서버나 데이터베이스에 접근할 수 있습니다.
3️⃣ 중앙 집중 관리.
WAN(Wide Area Network, 광역 통신망)은 네트워크의 중앙 관리를 가능하게 합니다.
본사에서 지사 네트워크 관리하거나, 중앙 서버에서 네트워크 자원을 통합적으로 관리할 수 있습니다.
4️⃣ 확장성.
WAN(Wide Area Network, 광역 통신망)은 필요한 만큼 확장할 수 있습니다.
지역이 다르거나 국가가 다르더라도, 네트워크를 확장하여 연결할 수 있습니다.
5️⃣ WAN의 단점.
1️⃣ 높은 비용.
WAN(Wide Area Network, 광역 통신망)을 구축하고 유지하는 데 비용이 많이 듭니다.
전용선, 위성 링크, 광섬유 등의 인프라를 구축하는 데 많은 자원이 필요합니다.
특히, 장거리 연결일수록 비용이 더 높아집니다.
2️⃣ 느린 속도.
WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)에 비해 데이터 전송 속도가 느릴 수 있습니다.
물리적 거리가 멀수록 데이터 전송에 걸리는 시간이 증가하고, 지연이 발생할 수 있습니다.
3️⃣ 복잡성.
WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)보다 구축과 관리가 훨씬 복잡합니다.
장거리 네트워크 통신은 여러 기술과 장비의 조합이 필요하며, 문제가 발생했을 때 해결하기 어렵습니다.
4️⃣ 보안 문제.
WAN(Wide Area Network, 광역 통신망)은 인터넷과 같은 공개 네트워크를 사용할 때 보안 문제가 발생할 수 있습니다.
데이터를 안전하게 보호하기 위해서는 암호화, 방화벽 등의 보안 기술이 필요합니다.
6️⃣ 결론.
WAN(Wide Area Network, 광역 통신망)은 광범위한 지역을 연결하는 네트워크로, LAN(Local Area Network, 근거리 통신망)과 MAN(Metropolitan Area Network, 도시권 통신망)을 연결하고 국가 간, 대륙 간 통신을 가능하게 합니다.
WAN(Wide Area Network, 광역 통신망)은 인터넷을 통해 여러 장치와 네트워크가 서로 연결될 수 있는 방법을 제공하며, 기업이나 정부, 은행 등에서 다양한 용도로 사용됩니다.
WAN(Wide Area Network, 광역 통신망)을 통해 먼 거리의 네트워크를 연결하여 자원 공유와 통신을 할 수 있지만, 구축 및 유지 비용이 높고 관리가 복잡할 수 있습니다.
-
🌐[Network] HTTP 프로토콜이란 무엇일까요?
🌐[Network] HTTP 프로토콜이란 무엇일까요?
HTTP(하이퍼텍스트 전송 프로토콜, HyperText Transfer Protocol)는 웹에서 클라이언트와 서버 간에 데이터를 주고받기 위한 규약입니다.
인터넷에서 웹 페이지, 이미지, 동영상 등 다양한 리소스를 전송하는 표준 통신 프로토콜로, 웹 브라우저와 웹 서버 간의 통신에 사용됩니다.
🙋♂️ 프로토콜(Protocol)이란 무엇일까요?
1️⃣ HTTP의 기본 개념.
1️⃣ 클라이언트-서버 모델.
HTTP는 클라이언트와 서버 간의 요청-응답 방식으로 동작합니다.
웹 브라우저 같은 클라이언트가 서버에 요청을 보내면, 서버는 요청을 처리하고 그 결과를 응답으로 돌려줍니다.
클라이언트 : 웹 브라우저, 모바일 앱 등 사용자 측에서 요청을 보내는 쪽입니다.
서버 : 클라이언트의 요청을 처리하고 응답을 보내는 쪽으로, 웹 서버나 데이터베이스 서버가 대표적입니다.
2️⃣ 무상태성(Stateless).
HTTP는 무상태성 프로토콜입니다.
즉, 각 요청은 독립적이며, 이전 요청이나 응답에 의존하지 않습니다.
서버는 요청을 처리할 때 클라이언트의 상태를 기억하지 않으며, 모든 요청은 새로운 것처럼 처리됩니다.
이를 보완하기 위해, 쿠키나 세션을 통해 클라이언트 상태를 유지하는 방법이 사용되기도 합니다.
3️⃣ URI(Uniform Resource Identifier)
HTTP는 URI(통합 지원 식별자)를 사용하여 웹에서 특정 리소스를 식별합니다.
일반적으로는 URI는 웹 주소(또는 URL)로 표현되며, 서버에서 원하는 리소스를 가리킵니다.
예: https://www.example.com/index.html
https:// : HTTP 프로토콜(HTTPS)
www.example.com : 서버 도메인 이름
/index.html : 서버 내에서 요청한 리소스의 경로
4️⃣ HTTP 메서드(HTTP Methods)
HTTP는 리소스에 대한 요청의 의도를 명확히 하기 위해 여러 메서드를 사용합니다.
메서드는 클라이언트가 서버에 어떤 작업을 요청하는지를 정의합니다.
주요 HTTP 메서드
GET : 리소스를 조회하는 데 사용합니다.(데이터를 서버에서 가져옴)
POST : 서버에 새로운 리소스를 생성하거나 데이터를 전송하는 데 사용합니다.
PUT : 서버의 기존 리소스를 수정하는 데 사용합니다.
DELETE : 서버에서 리소스를 삭제 하는 데 사용합니다.
HEAD : GET 요청과 유사하지만, 응답 본문 없이 헤더만을 반환합니다.
5️⃣ HTTP 상태 코드(HTTP Status Codes)
서버는 클라이언트의 요청에 대해 상태 코드를 통해 응답의 결과를 전달합니다.
상태 코드는 요청이 성공했는지, 실패했는지, 또는 어떤 문제가 발생했는지에 대한 정보를 제공하여, 3자리 숫자로 구성됩니다.
주요 상태 코드
200 OK : 요청이 성공적으로 처리됨.
201 Created : 리소스가 성공적으로 생성됨.
400 Bad Request : 클라이언트의 요청이 잘못됨.
401 Unauthorized : 인증이 필요함.
403 Forbidden : 권한이 없어서 요청이 거부됨.
404 Not Found : 요청한 리소스를 찾을 수 없음.
500 Internal Server Error : 서버 내부 오류가 발생함.
6️⃣ 헤더(Headers)
HTTP 요청과 응답에는 헤더가 포함됩니다.
헤더는 요청이나 응답에 대한 부가 정보를 담고 있으며, 클라이언트와 서버 간의 통신 방식을 제어합니다.
주요 헤더 예
Content-Type : 요청 또는 응답 본문의 데이터 형식을 지정합니다.
예: application/json, text/html
User-Agent : 클라이언트 애플리케이션(예: 브라우저)의 정보를 서버에 전달합니다.
Authorization : 인증에 필요한 정보(토큰 등)를 포함하여 서버에 전달합니다.
7️⃣ HTTP 버전.
HTTP는 여러 버전이 존재하며, 각 버전은 성능 및 기능을 개선해왔습니다.
HTTP/1.0 : 기본적인 기능만 제공하며, 한 번에 하나의 요청만 처리할 수 있습니다.
HTTP/1.1 : 연결 재사용(Persistent Connection)을 지원하여 여러 요청을 동시에 처리할 수 있습니다.
여전히 웹에서 가장 많이 사용되는 버전입니다.
HTTP/2 : 성능 향상을 위해 요청과 응답을 다중화하여, 동시에 여러 요청을 보내고 받을 수 있게 설계되었습니다.
웹 페이지 로딩 속도를 크게 개선합니다.
HTTP/3 : 최신 버전으로, QUIC 프로토콜을 기반으로 하여 성능과 보안성을 크게 개선했습니다.
📝 QUIC(Quick UDP Internet Connections) 프로토콜.
QUIC(Quick UDP Internet Connections)는 구글이 개발한 인터넷 전송 계층 프로토콜로, TCP와 같은 신뢰성있는 데이터 전송을 제공하면서도 UDP(User Datagram Protocol)를 기반으로 하여 더 빠른 연결 수립과 더 나은 성능을 목표로 합니다.
HTTP/3는 QUIC 위에서 동작하며, 웹에서의 저지연과 안정성을 개선하는 데 중요한 역할을 합니다.
2️⃣ HTTP 통신 과정.
HTTP 통신 과정은 클라이언트가 서버에 요청(Request)을 보내고, 서버가 응답(Response)을 반환하는 요청-응답 모델로 구성됩니다.
1️⃣ 클라이언트 요청.
클라이언트가 서버에 리소스를 요청하면, HTTP 요청을 서버로 전송합니다.
요청은 다음과 같은 구성 요소를 포함합니다.
요청 메서드(예: GET, POST)
URI(예: /index.html)
헤더(예: Content-Type, User-Agent)
본문(POST 요청일 경우 데이터가 포함될 수 있음)
👉 예시: GET 요청.
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
2️⃣ 서버 응답.
서버는 클라이언트 요청을 처리한 후, HTTP 응답을 클라이언트에 반환합니다.
응답은 다음과 같은 구성 요소를 포함합니다.
상태 코드 (예: 200 OK, 404 Not Found)
헤더 (예: Content-Type, Cache-Control)
본문 (예: HTML, JSON 데이터)
👉 예시: 200 OK 응답.
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1354
<html>
<head><title>Example</title></head>
<body>...</body>
</html>
3️⃣ HTTP의 주요 특징.
1️⃣ 간단함.
HTTP는 텍스트 기반 프로토콜로, 사람이 이해하기 쉬운 방식으로 설계되었습니다.
각 요청과 응답은 명확한 구조로 되어 있어, 쉽게 해석할 수 있습니다.
2️⃣ 확장성.
HTTP는 확장 가능한 프로토콜입니다.
헤더를 통한 다양한 정보(인증 정보, 캐싱 정보, 데이터 형식 등)를 전송할 수 있으며, 새로운 기능도 쉽게 추가할 수 있습니다.
3️⃣ 유연성.
HTTP는 다양한 데이터 형식을 전송할 수 있습니다.
텍스트, 이미지, 동영상 등 다양한 콘텐츠 타입을 지원하며, 클라이언트와 서버가 이를 교환할 수 있습니다.
4️⃣ 무상태성.
각 HTTP 요청은 독립적으로 처리되며, 서버는 요청 간에 상태를 유지하지 않습니다.
이를 통해 서버는 확장성과 단순성을 유지할 수 있지만, 쿠키나 세션을 통해 상태 유지를 보완할 수 있습니다.
4️⃣ HTTP와 HTTPS의 차이
HTTPS(HyperText Transfer Protocol Secure)는 HTTP에 보안 계층(SSL/TLS)이 추가된 버전입니다.
HTTPS는 데이터를 암호화하여 전송하므로, 클라이언트와 서버 간의 통신을 보호할 수 있습니다.
이를 통해 데이터가 제 3자에 의해 도청, 변조, 위조되는 것을 방지합니다.
👉 주요 차이점.
HTTP : 데이터를 암호화하지 않고 평문으로 전송.
HTTPS : SSL/TLS를 사용하여 데이터를 암호화해 전송.
5️⃣ 결론.
HTTP는 웹에서 클라이언트와 서버 간에 데이터를 주고받는 데 사용되는 핵심적인 통신 프로토콜입니다.
요청-응답 모델을 기반으로 하며, 리소스 접근, 데이터 전송, 웹 페이지 로딩 등에 사용됩니다.
HTTP는 간단하고 확장 가능하며, 현대 웹의 기반을 이루는 중요한 프로토콜입니다.
-
🌐[Network] LAN이 합쳐져서 WAN이 되는걸까?
🌐[Network] LAN이 합쳐져서 WAN이 되는걸까?
네트워크 개념에서 LAN(Local Area Network, 근거리 통신망)이 WAN(Wide Area Network, 광역 통신망)으로 단순히 합쳐지는 것은 아닙니다.
LAN과 WAN은 서로 다른 규모와 기능을 가진 네트워크 유형이며, 그 관계는 단순한 결합 이상의 개념입니다.
1️⃣ LAN과 WAN의 차이와 관계.
1️⃣ LAN(Local Area Network, 근거리 통신망)
LAN(Local Area Network, 근거리 통신망)은 지리적 범위(예: 집, 사무실, 학교) 내에서 컴퓨터, 프린터, 서버 등 장치들을 연결하는 네트워크입니다.
고속 데이터 전송을 제공하며, 주로 이더넷(Ethernet) 또는 Wi-Fi를 사용하여 장치 간의 통신을 처리합니다.
2️⃣ WAN(Wide Area Network, 광역 통신망)
WAN(Wide Area Network, 광역 통신망)은 넓은 지리적 범위(예: 여러 도시, 국가, 심지어 전 세계)를 커버하는 네트워크입니다.
WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)보다 느릴 수 있으며, 여러 LAN(Local Area Network, 근거리 통신망)을 연결하여 원거리 통신을 가능하게 합니다.
WAN(Wide Area Network, 광역 통신망)은 ISP(Internet Service Provider, 인터넷 서비스 제공업체) 또는 전용선을 통해 연결되며, 네트워크 장치 간의 물리적 거리가 매우 멀 수 있습니다.
🙋♂️ WAN(Wide Area Network)이란 무엇일까요?
🙋♂️ 이더넷(Ethernet)이란 무엇일까요?
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
2️⃣ LAN과 WAN의 관계.
👉 LAN(Local Area Network, 근거리 통신망)이 WAN(Wide Area Network, 광역 통신망)에 연결된다.
여러 LAN(Local Area Network, 근거리 통신망)을 WAN(Wide Area Network, 광역 통신망)으로 연결하여 광범위한 네트워크를 형성합니다.
예를 들어, 한 기업의 각 지사(각 지사는 LAN(Local Area Network, 근거리 통신망)을 형성)를 WAN(Wide Area Network, 광역 통신망)을 통해 연결하여 본사와 다른 지사 간에 통신할 수 있게 합니다.
👉 WAN(Wide Area Network, 광역 통신망)은 LAN(Local Area Network, 근거리 통신망)을 포함할 수 있다.
WAN(Wide Area Network, 광역 통신망)은 여러 LAN(Local Area Network, 근거리 통신망)을 포함할 수 있습니다.
즉, WAN(Wide Area Network, 광역 통신망)의 일부는 여러 개의 LAN(Local Area Network, 근거리 통신망)을 연결하여, 전세계에 분산된 장치들이 서로 통신할 수 있게 만듭니다.
3️⃣ 예시
1️⃣ 회사 네트워크.
한 회사가 서울, 부산, 뉴욕에 지사를 두고 있다고 가정합니다.
각 지사는 자체적인 LAN(Local Area Network, 근거리 통신망)을 가지고 있는데, 이 LAN(Local Area Network, 근거리 통신망)은 지사 내의 컴퓨터, 프린터, 서버 등을 연결합니다.
각 지사는 WAN(Wide Area Network, 광역 통신망)을 통해 본사와 다른 지사들과 연결됩니다.
WAN(Wide Area Network, 광역 통신망)을 통해 서울 지사의 직원이 부산 지사의 서버에 접근하거나, 뉴욕 지사와 파일을 공유할 수 있습니다.
2️⃣ 인터넷(Internet).
인터넷(Internet)은 세계 최대의 WAN(Wide Area Network, 광역 통신망)으로, 여러 LAN(Local Area Network, 근거리 통신망)과 WAN(Wide Area Network, 광역 통신망)을 연결하여 전 세계의 컴퓨터들이 서로 통신할 수 있도록 만듭니다.
내가 집에서 인터넷에 접속하는 경우, 집의 LAN(예: Wi-Fi 네트워크)이 인터넷(WAN, Wide Area Network)을 통해 전 세계의 다른 컴퓨터와 연결됩니다.
4️⃣ WAN(Wide Area Network, 광역 통신망)의 구현 방법.
WAN(Wide Area Network, 광역 통신망)을 구현하기 위해서는 ISP(Internet Service Provider, 인터넷 서비스 제공자), 위성 링크, 전용선, 광섬유 등의 기술이 사용됩니다.
LAN(Local Area Network, 근거리 통신망)은 일반적으로 스위치, 라우터, 케이블을 통해 설정되지만, WAN(Wide Area Network)은 훨씬 더 복잡한 기술 인프라를 필요로 합니다.
VPN(가상 사설망)도 WAN(Wide Area Network, 광역 통신망)을 구축하는 방법 중 하나로, 여러 LAN을 공용 네트워크(인터넷, Internet)를 통해 안전하게 연결할 수 있도록 해줍니다.
5️⃣ 결론.
LAN(Local Area Network, 근거리 통신망)이 여러개 합쳐져서 WAN(Wide Area Network, 광역 통신망)이 되는 것은 아닙니다.
그러나 LAN(Local Area Network, 근거리 통신망)은 WAN(Wide Area Network, 광역 통신망)의 일부가 될 수 있으며, WAN(Wide Area Network, 광역 통신망)은 여러 LAN(Local Area Network, 근거리 통신망)을 연결하여 광범위한 통신망을 형성합니다.
WAN(Wide Area Network, 광역 통신망)은 주로 먼 거리의 네트워크를 연결하기 위한 것이고, LAN(Local Area Network, 근거리 통신망)은 가까운 거리에서 효율적으로 자원을 공유하고 데이터를 전송하기 위해 설계된 네트워크입니다.
-
🌐[Network] RESTful API란 무엇일까요?
🌐[Network] RESTful API란 무엇일까요?
RESTful API는 REST(Representational State Transfer) 아키텍처 스타일을 기반으로 한 웹 API를 의미합니다.
RESTful API는 클라이언트와 서버 간의 통신을 효율적이고 일관성 있게 설계하기 위해 HTTP 프로토콜을 활용하여 데이터를 주고 받는 방식입니다.
RESTful은 특정 기술이나 프로토콜을 지칭하는 것은 아니지만, REST 아키텍처의 원칙을 준수하는 API를 설명할 때 사용하는 용어입니다.
1️⃣ REST(Representational State Transfer)의 기본 개념.
REST는 Roy Fielding이 2000년 박사 논문에서 소개한 분산 시스템을 설계하기 위한 아키텍처 스타일입니다.
RESTful API는 이 원칙을 따르는 API로, 클라이언트-서버 구조, 무상태성, 캐싱 가능성, 계층화된 시스템과 같은 REST의 주요 제약을 따릅니다.
2️⃣ REST의 주요 원칙 및 RESTful API의 특징.
1️⃣ 자원(Resource) 기반.
REST에서는 모든 것을 자원으로 간주합니다.
예를 들어, 사용자는 하나의 자원, 게시물은 또 다른 하나의 자원으로 취급됩니다.
각 자원은 고유한 URI(Uniform Resource Identifer)를 통해 식별됩니다.
예를 들어, https://api.example.com/user/1는 id=1인 사용자 자원을 나타냅니다.
👉 URI 설계 예시
GET /users -> 모든 사용자 목록 가져오기
GET /users/1 -> 특정 사용자(id=1) 가져오기
POST /users/1 -> 새 사용자 생성
PUT /users/1 -> 사용자 정보 업데이트
DELETE /users/1 -> 사용자 삭제
2️⃣ HTTP 메서드 사용.
RESTful API는 HTTP 메서드를 자원의 작업과 연관시키기 위해 사용합니다.
주요 메서드로는 GET, POST, PUT, DELETE가 있습니다.
GET : 자원을 조회할 때 사용합니다.
POST : 자원을 생성할 때 사용합니다.
PUT : 자원을 업데이트할 때 사용합니다.
DELETE : 자원을 삭제할 때 사용합니다.
이러한 HTTP 메서드의 사용은 요청의 목적을 명확히 하고, 클라이언트와 서버 간의 상호작용을 일관성 있게 만듭니다.
3️⃣ 무상태성(Stateless)
RESTful API는 무상태(Stateless)해야 합니다.
즉, 서버는 클라이언트의 상태를 저장하지 않습니다.
클라이언트가 보낸 각각의 요청은 독립적이어야 하며, 요청에 의존해서는 안 됩니다.
클라이언트는 매 요청마다 필요한 모든 정보를 서버에 보내야 하며, 서버는 해당 요청만을 처리하고 응답을 반환합니다.
4️⃣ 캐시 가능(Cacheable)
RESTful API에서 응답은 캐시 될 수 있어야 합니다.
서버는 응답에 대한 캐싱 가능 여부를 클라이언트에게 명확히 전달합니다.
클라이언트가 자원의 불필요한 요청을 반복하지 않도록 하기 위해, 서버는 HTTP 헤더에 Cache-Control이나 Expires와 같은 캐시 관련 정보를 포함하여 응답을 보낼 수 있습니다.
5️⃣ 계층화된 구조.
RESTful 아키텍처는 계층화된 시스템을 지원합니다.
클라이언트는 중간 서버(프록시, 게이트웨이 등)를 통과하더라도, 요청과 응답이 처리되는 방식에 대해 신경 쓸 필요가 없습니다.
서버는 여러 계층을 통해 보안, 로드 밸런싱 등을 처리할 수 있습니다.
6️⃣ 일관된 인터페이스.
RESTful API는 일관된 인터페이스를 제공해야 합니다.
이는 자원에 접근하고 조작하는 방식이 명확하고 일관적이어야 하며, 클라이언트가 각 자원에 대해 동일한 방식으로 작업을 수행할 수 있어야 한다는 의미입니다.
7️⃣ 표현(Representation) 전송.
자원 자체는 서버에 저장되며, 클라이언트는 서버에서 자원의 표현(Representation) 을 전송받습니다.
일반적으로 RESTful API는 JSON이나 XML 형식으로 자원의 상태를 표현하며, 이를 클라이언트에 전달합니다.
👉 예시: 서버가 사용자 자원을 클라이언트에게 JSON 형식으로 응답할 때.
{
"id": 1,
"name": "Kobe",
"email": "kobe@example.com"
}
3️⃣ RESTful API의 HTTP 메서드와 자원의 관계.
1️⃣ GET
자원을 조회하는 데 사용됩니다.
예: GET /users/1은 id=1인 사용자를 조회하는 요청입니다.
2️⃣ POST
새로운 자원을 생성하는 데 사용됩니다.
예: POST /users는 새로운 사용자를 생성하는 요청입니다.
요청 본문에는 생성할 자원의 정보가 포함됩니다.
3️⃣ PUT
기존 자원을 업데이트하는 데 사용됩니다.
예: PUT /users/1은 id=1인 사용자 정보를 업데이트하는 요청입니다.
4️⃣ DELETE
자원을 삭제하는 데 사용됩니다.
예: DELETE /users/1은 id=1인 사용자를 삭제하는 요청입니다.
4️⃣ RESTful API의 예
👉 사용자 자원을 관리하는 RESTful API 예시.
1️⃣ GET 요청 (모든 사용자 조회)
GET /users
응답
[
{"id": 1, "name": "Kobe", "email": "kobe@example.com"},
{"id": 2, "name": "Eric", "email": "eric@example.com"}
]
2️⃣ POST 요청 (새 사용자 생성)
POST /users
요청 본문.
{
"name": "Kobe",
"email": "kobe@example.com"
}
응답.
PUT /users/3
3️⃣ PUT 요청 (사용자 정보 업데이트)
PUT /users/3
요청 본문
{
"name": "Kobe updated",
"email": "kobe.updated@example.com"
}
응답
{
"id": 3,
"name": "Kobe updated",
"email": "kobe.updated@example.com"
}
4️⃣ DELETE 요청 (사용자 삭제)
DELETE /users/3
응답.
{
"message": "User deleted successfully"
}
5️⃣ RESTful API의 장점.
1️⃣ 확장성.
RESTful API는 서버와 클라이언트 간의 결합도를 낮춰, 확장 가능하고 유연한 시스템을 설계할 수 있습니다.
2️⃣ 표준화.
HTTP 프로토콜의 표준을 기반으로 하여, 다양한 클라이언트와 쉽게 연동이 가능합니다.
3️⃣ 경량성.
JSON 같은 경량 포맷을 사용해 데이터 전송을 최적화할 수 있습니다.
4️⃣ 캐싱 가능.
HTTP 캐싱 메커니즘을 통해 성능을 향상시키고 네트워크 부하를 줄일 수 있습니다.
6️⃣ 결론.
RESTful API는 웹 애플리케이션에서 서버와 클라이언트 간의 상호작용을 일관성 있고 효율적으로 처리하기 위한 API 설계 방식입니다.
HTTP 프로토콜을 기반으로 자원을 CRUD 방식으로 관리하며, 무상태성, 자원 기반 접근, 일관된 인터페이스 등의 REST 원칙을 따릅니다.
RESTful API는 다양한 클라이언트(모바일, 웹 등)와 통신할 수 있는 확장 가능하고 유연한 시스템을 구축하는 데 매우 유용합니다.
-
🌐[Network] 프로토콜(Protocol)이란 무엇일까요?
🌐[Network] 프로토콜(Protocol)이란 무엇일까요?
프로토콜(Protocol) 은 컴퓨터나 네트워크 장치간에 데이터를 주고 받기 위한 일련의 규칙과 절차를 정의한 표준입니다.
쉽게 말해, 통신하는 장치들이 서로 이해하고 데이터 교환을 원활히 할 수 있도록 정해진 약속을 말합니다.
프로토콜은 데이터 형식, 통신 속도, 오류 처리 방식, 메시지 구조 등 통신에 필요한 모든 요소를 정의합니다.
1️⃣ 프로토콜의 주요 역할.
1️⃣ 통신 규칙 제공.
서로 다른 장치나 시스템이 데이터를 주고받기 위해 따라야 할 규칙을 제공합니다.
각 장치나 시스템이 동일한 프로토콜을 따름으로써, 서로 원활하게 정보를 주고받을 수 있습니다.
2️⃣ 데이터 형식 정의.
데이터를 어떻게 표현하고 전송할지에 대한 형식을 정의합니다.
예를 들어, 메시지의 시작과 끝을 어떻게 구분할지, 데이터의 크기나 구조는 어떻게 할지 등을 결정합니다.
3️⃣ 오류 검출 및 수정.
데이터 전송 중 발생할 수 있는 오류를 감지하고, 이를 처리하는 방식을 정의합니다.
통신 중 데이터가 손상되거나 유실될 경우 이를 해결하기 위한 절차가 포함됩니다.
4️⃣ 동기화
데이터를 주고 받는 송신자와 수신자 간의 동기화를 처리하여, 두 장치가 서로의 데이터를 올바르게 이해하고 처리할 수 있도록 돕습니다.
2️⃣ 프로토콜의 종류.
네트워크 통신에서 사용되는 프로토콜은 OSI(Open Systems Interconnection) 모델이나 TCP/IP 모델의 계층에 따라 나눌 수 있습니다.
각 계층은 서로 다른 프로토콜을 사용하여 특정 기능을 수행합니다.
1️⃣ 네트워크 계층별 프로토콜.
프로토콜은 네트워크 통신의 각 단계에 해당하는 다양한 역할을 담당합니다.
👉 응용 계층(Application Layer)
HTTP(HyperText Transfer Protocol)
웹 브라우저와 웹 서버 간에 데이터를 주고받기 위한 프로토콜로, 웹 페이지의 전송을 담당합니다.
FTP(File Transfer Protocol)
컴퓨터 간에 파일을 전송하는 데 사용되는 프로토콜입니다.
SMTP(Simple Mail Transfer Protocol)
이메일 전송을 위한 프로토콜입니다.
DNS(Domain Name System)
도메인 이름을 IP 주소로 변환하는 프로토콜입니다.
👉 전송 계층(Transport Layer)
TCP(Transmission Control Protocol)
신뢰성 있는 연결형 데이터 전송 프로토콜로, 데이터를 패킷으로 나누어 전송하고, 오류가 발생하면 재전송을 통해 안정성을 보장합니다.
UDP(User Datagram Protocol)
비연결형 프로토콜로, 빠른 데이터 전송이 가능하지만, 데이터 전송의 신뢰성은 보장하지 않습니다.
주로 실시간 스트리밍이나 게임에서 사용됩니다.
👉 인터넷 계층(Internet Layer)
IP(Internet Protocol)
데이터를 패킷으로 나누어 목적지까지 전달하는 역할을 하는 프로토콜로, 인터넷에서 기본적인 데이터 전송을 담당합니다.
ICMP(Internet Control Message Protocol)
네트워크 장비 간의 진단이나 오류 메시지를 전송하는 데 사용됩니다.
예: ping 명령.
👉 네트워크 인터페이스 계층(Network Interface Layer)
Ethernet
로컬 영역 네트워크(LAN)에서 사용되는 물리적 네트워크 프로토콜로, 네트워크 카드 간 데이터 전송을 처리합니다.
Wi-Fi
무선 네트워크에서 사용되는 통신 프로토콜로, 무선 장치 간 데이터를 주고받을 때 사용됩니다.
2️⃣ 보안 관련 프로토콜
SSL/TLS(Secure Sockets Layer / Transport Layer Security)
인터넷 상에서 데이터를 안전하게 암호화하여 전송하는 프로토콜입니다.
HTTPS는 SSL/TLS를 사용하여 HTTP 통신을 암호화합니다.
SSH(Secure Shell)
원격 시스템에 안전하게 접속하기 위한 프로토콜로, 데이터를 암호화하여 전송합니다.
3️⃣ 프로토콜 예시: HTTP 프로토콜
HTTP(HyperText Transfer Protocol) 는 클라이언트와 서버간의 웹 페이지 데이터를 주고받는 데 사용하는 프로토콜입니다.
클라이언트(예: 웹 브라우저)가 HTTP 요청을 보내면, 서버는 그에 대한 응답을 HTTP 형식으로 반환합니다.
클라이언트의 요청(Request) : GET /index.html HTTP/1.1
서버의 응답(Response) : HTTP/1.1 200 OK
이때, HTTP 프로토콜은 요청과 응답의 구조, 상태 코드(200, 404 등), 메서드(GET, POST 등) 등 모든 통신 규칙을 정의합니다.
4️⃣ 프로토콜의 필요성.
1️⃣ 호환성 확보.
다양한 장치와 시스템 간의 호환성을 보장하기 위해 표준화된 프로토콜이 필요합니다.
프로토콜이 없다면 서로 다른 장치나 시스템 간에 데이터를 주고받는 것이 매우 어려워질 것입니다.
2️⃣ 통신 표준 제공.
프로토콜은 데이터 형식, 에러 처리 방식, 전송 속도 등 여러 요소를 표준화하여 네트워크 상의 모든 장치가 동일한 규칙을 따르도록 합니다.
이를 통해 전 세계적으로 인터넷 통신이 가능해집니다.
3️⃣ 효율적인 통신.
프로토콜은 데이터 전송을 최적화하고, 오류 발생 시 복구할 수 있는 메커니즘을 제공합니다.
또한 통신 과정에서 발생할 수 있는 여러 문제를 해결할 수 있는 방법을 정의하여 안정적이고 효율적인 데이터 전송을 보장합니다.
5️⃣ 결론.
프로토콜(Protocol) 은 장치들 간의 통신을 위한 규칙과 표준을 정의한 중요한 요소입니다.
프로토콜이 없다면 네트워크 상의 다양한 시스템 간의 데이터 교환은 불가능할 것입니다.
인터넷과 네트워크 통신에서 프로토콜은 데이터의 정확한 전달을 보장하고, 네트워크 장치 간의 상호 운용성을 유지하기 위한 필수적인 구성 요소입니다.
-
💾 [CS] API에서의 인터페이스(Interface)란 무엇일까?
💾 [CS] API에서의 인터페이스(Interface)란 무엇일까?
API에서의 인터페이스(Interface)는 프로그램 간 상호작용을 위한 규칙이나 방법을 정의한 것을 의미합니다.
여기서 인터페이스(Interface)는 API(응용 프로그래밍 인터페이스, Application Programming Interface)가 제공하는 기능을 다른 소프트웨어나 시스템이 어떻게 이용할 수 있는지를 규정한 명세라고 할 수 있습니다.
1️⃣ API에서의 인터페이스 정의
인터페이스는 시스템이 제공하는 기능을 외부에서 어떻게 사용할 수 있는지에 대한 규칙과 구조를 설명합니다.
이는 함수, 메소드, 데이터 형식, 호출 방법 등 다양한 요소로 구성될 수 있으며, API를 통해 소프트웨어 컴포넌트 간의 상호작용을 가능하게 합니다.
1️⃣ 주요 특징.
1️⃣ 함수나 메소드 호출 규칙.
API는 외부 프로그램이 호출할 수 있는 함수나 메소드를 정의합니다.
인터페이스는 이 함수나 메소드가 어떻게 호출되고, 어떤 인수가 필요한지, 그리고 반환 값이 무엇인지를 명시합니다.
예를 들어, 특정 API에서 데이터를 가져오는 함수가 있다면, 그 함수가 어떤 입력 값을 필요로 하고, 어떤 형식의 데이터를 반환하는지 정의 된 것이 인터페이스 입니다.
2️⃣ 명세화된 입력과 출력.
API 인터페이스는 데이터 형식과 구조를 명확히 정의합니다.
이는 개발자가 API와 상호작용할 때 어떤 형식의 입력과 출력을 처리해야 하는지 이해하는 데 필수적입니다.
예를 들어, API의 한 함수가 JSON 형식의 입력 데이터를 받고 XML 형식으로 출력을 반환한다면, 이 모든 사항은 인터페이스에 명시되어 있어야 합니다.
3️⃣ 추상화.
인터페이스는 API의 내부 구현을 추상화하고, 외부 사용자에게는 필요한 기능만을 제공하는 구조입니다.
API를 사용하는 개발자는 그 내부가 어떻게 동작하는지 알 필요가 없으며, 정해진 규격대로 호출하면 원하는 결과를 얻을 수 있습니다.
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’이란 무엇일까?
4️⃣ 계약(Contract)
API에서의 인터페이스는 일종의 계약으로 간주될 수 있습니다.
API는 사용자에게 특정 기능을 제공하기 위한 약속을 하며, 사용자는 그 약속된 대로 인터페이스를 사용합니다.
이를 통해 서로 다른 시스템 간의 원활한 통신이 가능해집니다.
2️⃣ API에서의 인터페이스의 역할.
1️⃣ 서비스 제공.
API는 시스템이 제공하는 기능을 외부에 공개하여, 다른 시스템이나 프로그램이 그 기능을 활용할 수 있게 합니다.
이 과정에서 인터페이스는 어떤 기능이 제공되는지, 어떻게 사용해야 하는지를 설명하는 설계도 역할을 합니다.
2️⃣ 호환성 보장.
API 인터페이스는 시스템 간의 상호작용에서 호환성을 보장하는 역할을 합니다.
한 시스템이 제공하는 API를 외부 시스템이 이용할 때, 인터페이스를 준수함으로써 서로 다른 시스템 간에도 일관된 상호작용이 가능해집니다.
3️⃣ 사용자와 시스템 간의 경계 설정.
API 인터페이스는 외부 사용자가 접근할 수 있는 기능과, 내부 시스템에서만 사용되는 기능을 구분하는 경계선 역할을 합니다.
이는 보안 측면에서도 중요하며, 사용자는 인터페이스를 통해 제공된 기능만을 사용할 수 있습니다.
4️⃣ 재사용성.
인터페이스는 동일한 규격을 따르므로, 한 번 정의된 API는 다양한 프로그램이나 시스템에서 재사용될 수 있습니다.
이를 통해 개발 시간과 비용을 절감할 수 있습니다.
3️⃣ 예시: RESTful API의 인터페이스.
RESTful API를 예로 들면, 인터페이스는 주로 HTTP 메소드(GET, POST, PUT, DELETE 등)를 사용하여 리소스에 접근하는 방법을 규정합니다.
GET 요청 : 특정 데이터를 가져오기 위한 규칙을 정의
POST 요청 : 데이터를 생성하는 방법.
PUT 요청 : 데이터를 업데이트하는 방법.
DELETE 요청 : 데이터를 삭제하는 방법.
RESTful API의 인터페이스는 요청에 따라 어떤 경로(URL)를 사용할지, 어떤 매개변수와 헤더가 필요한지, 그리고 응답이 어떤 형태로 돌아올지를 규정합니다.
4️⃣ 인터페이스와 프로토콜의 차이점.
인터페이스
기능을 제공하는 방식과 규칙을 설명합니다.
어떨게 데이터를 입력하고, 결과를 출력받는지에 대한 약속입니다.
프로토콜
데이터가 어떻게 전송되는지에 대한 규칙을 설명합니다.
통신 규칙을 다루며, 전송 방식에 중점을 둡니다.
예를 들어, HTTP나 TCP/IP는 프로토콜입니다.
5️⃣ 결론.
API에서 인터페이스는 프로그램 간 상호작용을 위한 명세로, 외부 시스템이나 소프트웨어가 어떻게 기능을 호출하고 사용할 수 있는지를 규정합니다.
이는 소프트웨어 구성 요소 간의 계약처럼 동작하며, 사용자와 시스템 간의 경계를 설정하고 호환성을 보장하여 원활한 통신을 가능하게 합니다.
-
💾 [CS] API에서의 인터페이스와 소프트웨어 공학에서의 인터페이스 개념.
💾 [CS] API에서의 인터페이스와 소프트웨어 공학에서의 인터페이스 개념.
API에서의 인터페이스와 소프트웨어 공학에서의 인터페이스는 유사한 개념을 공유하지만, 구체적인 사용 맥락과 범위에서 약간의 차이가 있습니다.
두 개념 모두 추상화된 상호작용의 규칙을 정의한다는 공통점이 있지만, 다음과 같은 차이점과 유사점이 있습니다.
1️⃣ 공통점.
1️⃣ 추상화된 상호작용 규칙.
두 개념 모두 상호작용하는 시스템이나 모듈 간의 규칙을 정의합니다.
API와 소프트웨어 공학에서의 인터페이스 모두 외부에서 내부의 구현 세부 사항을 알 필요 없이 제공된 규칙에 따라 특정 기능을 사용할 수 있도록 합니다.
2️⃣ 계약(Contract) 역할.
계약처럼, 인터페이스는 어떤 기능이 제공되고, 그 기능이 어떻게 호출되는지를 규정합니다.
사용자는(또는 클라이언트)는 이 계약에 따라 인터페이스를 호출하고 사용하게 되며, 내부 구현과는 독립적으로 동작할 수 있습니다.
3️⃣ 구현의 독립성.
API와 소프트웨어 공학에서의 인터페이스 모두 구현에 독립적입니다.
이는 사용자가 인터페이스를 통해 기능을 호출하지만, 구현이 어떻게 이루어지는지는 중요하지 않다는 원칙을 따릅니다.
이를 통해 구현 변경이 이루어져도 인터페이스가 동일하게 유지된다면, 시스템 간의 상호작용에는 영향이 없습니다.
2️⃣ 차이점.
1️⃣ API에서의 인터페이스
범위
API에서의 인터페이스는 주로 시스템 간 상호작용을 적용합니다.
외부 애플리케이션이나 서비스가 제공하는 기능을 호출할 수 있는 방법을 제공하며, 이는 웹 서비스(API)나 외부 모듈과의 통신에 중점을 둡니다.
사용 맥락
주로 웹 API, 라이브러리 API 등에 사용되며, 클라이언트 API 제공자의 규격에 맞춰 기능을 사용할 수 있도록 합니다.
API 인터페이스는 주로 함수, 메서드, HTTP 요청 등을 통한 상호작용을 다룹니다.
예: RESTful API에서의 HTTP 메서드(GET, POST 등), API 호출 시 제공되는 엔드포인트와 파라미터, 그리고 반환되는 데이터 형식 등이 API 인터페이스에 포함됩니다.
2️⃣ 소프트웨어 공학에서의 인터페이스
범위
소프트웨어 공학에서의 인터페이스는 클래스나 모듈 간의 상호작용을 정의합니다.
인터페이스는 객체 지향 프로그래밍(OOP)에서 사용되며, 특정 클래스나 모듈이 어떤 메서드나 속성을 제공해야 하는지를 정의합니다.
사용 맥락
주로 객체 지향 언어(Java, C++, Python 등)에서 사용되는 개념으로, 인터페이스는 클래스의 행위 계약을 정의합니다.
클래스가 특정 인터페이스를 구현하면, 해당 인터페이스에서 요구하는 메서드를 모두 정의해야 합니다.
예시
Java에서의 인터페이스는 클래스가 구현해야 할 메서드의 목록을 정의하며, 이를 통해 다형성(Polymorphism)을 지원합니다.
예를 들어, 여러 클래스가 동일한 인터페이스를 구현하여 동일한 메서드를 다르게 정의할 수 있습니다.
3️⃣ 개념의 유사성과 차이점 요약.
API 인터페이스는 외부 시스템과의 상호작용을 위한 규칙을 정의하며, 주로 서비스 제공을 위해 시스템이나 애플리케이션에서 기능을 제공할 때 사용됩니다.
웹 API, 라이브러리 API 등 외부 프로그램이 호출할 수 있는 방법을 설명합니다.
소프트웨어 공학에서의 인터페이스는 객체 지향 프로그래밍에서 클래스나 모듈 간의 상호작용 규칙을 정의합니다.
주로 다형성과 구현 독립성을 지원하며, 여러 클래스가 동일한 인터페이스를 구현할 수 있도록 합니다.
4️⃣ 결론.
API에서의 인터페이스는 외부 시스템이 상호작용할 수 있도록 기능을 제공하는 방법을 규정하고, 소프트웨어 공학에서의 인터페이스는 클래스나 모듈 간의 계약으로서, 객체 지향 프로그래밍에서 다형성을 지원하는 데 중점을 둡니다.
두 개념은 상호작용 규칙을 정의하는 공통적인 개념을 공유하며, 구현과 추상화의 원칙에 기반하고 있지만, 사용되는 맥락과 범위는 다릅니다.
-
🌐[Network] 네트워크 인터페이스 카드(NIC, Network Interface Card)란 무엇일까요?
🌐[Network] 네트워크 인터페이스 카드(NIC, Network Interface Card)란 무엇일까요?”
네트워크 인터페이스 카드(NIC, Network Interface Card)
네트워크 인터페이스 카드(NIC, Network Interface Card) 는 컴퓨터나 네트워크 장치가 네트워크에 연결하여 데이터를 송수신할 수 있게 해주는 하드웨어 장치입니다.
무선 네트워크 어댑터
네트워크 인터페이스 카드(이하 NIC)는 이더넷 포트 또는 무선 네트워크 어댑터를 통해 로컬 네트워크(LAN, Local Area Network) 또는 인터넷(WAN, Wide Area Network) 과 같은 네트워크에 연결되며, 데이터를 전송하는 데 필요한 물리적인 인터페이스를 제공합니다.
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
1️⃣ 네트워크 인터페이스 카드(NIC, Network Interface Card)의 주요 역할.
1️⃣ 데이터 송수신.
NIC는 네트워크 상의 다른 장치와 데이터를 송수신하는 역할을 합니다.
컴퓨터에서 데이터를 패킷 형태로 만들어 네트워크를 통해 보낼 수 있으며, 다른 장치에서 온 데이터를 받아 컴퓨터가 처리할 수 있는 형태로 변환합니다.
2️⃣ 네트워크 연결.
NIC는 컴퓨터나 장치를 네트워크에 물리적으로 연결합니다.
유선 네트워크의 경우 이더넷 케이블을 통해 연결되며, 무선 네트워크의 경우 Wi-Fi를 통해 네트워크에 연결할 수 있습니다.
3️⃣ MAC 주소 할당.
NIC는 고유한 MAC 주소를 가지고 있습니다.
MAC 주소는 네트워크에서 장치를 식별하는 고유 식별자로, 네트워크 상에서 데이터 패킷을 송수신할 때 특정 장치를 구분하는 데 사용됩니다.
4️⃣ 데이터 변환
컴퓨터 내부에서 사용되는 디지털 신호를 네트워크에서 사용할 수 있는 신호(유선의 경우 전기 신호, 무선의 경우 무선 신호)로 변환하여 네트워크를 통해 데이터를 전송합니다.
반대로, 네트워크에서 받은 신호를 컴퓨터가 이해할 수 있는 디지털 신호로 변환하는 기능도 수행합니다.
2️⃣ 네트워크 인터페이스 카드(NIC, Network Interface Card)의 구성요소.
1️⃣ 이더넷 포트.
유선 네트워크 카드에는 이더넷 포트가 있어, 이더넷 케이블을 사용해 네트워크에 연결할 수 있습니다.
이더넷 포트는 RJ-45 커넥터를 사용하여 네트워크 케이블을 연결합니다.
2️⃣ 무선 안테나.
무선 네트워크 카드에는 Wi-Fi 신호를 주고 받기 위한 무선 안테나가 장착 되어 있습니다.
이를 통해 무선 액세스 포인트(AP, Access Point)와 통신하여 네트워크에 접속합니다.
3️⃣ 칩셋(Chipset).
NIC에는 데이터 전송 및 처리를 관리하는 칩셋(Chipset)이 장착되어 있습니다.
이 칩셋은 네트워크 프로토콜을 처리하고, 데이터를 패킷으로 전환하거나 수신된 패킷을 해석하는 역할을 합니다.
4️⃣ MAC 주소.
NIC는 네트워크에서 MAC 주소라는 고유한 하드웨어 주소를 가집니다.
이 MAC 주소는 네트워크 상에서 장치를 고유하게 식별하는 데 사용됩니다.
3️⃣ 네트워크 인터페이스 카드(NIC, Network Interface Card)의 유형.
1️⃣ 유선 네트워크 카드(Ethernet NIC).
유선 NIC는 이더넷 케이블을 통해 네트워크에 연결하는 카드입니다.
일반적으로 데스크탑 컴퓨터, 서버, 프린터 등에서 많이 사용되며, 고속 데이터 전송이 가능합니다.
이더넷 카드의 속도는 10Mbps, 100Mbps, 1Gbps, 10Gbps 등의 다양한 속도를 지원할 수 있으며, CAT5e, CAT6 등의 케이블을 사용합니다.
2️⃣ 무선 네트워크 카드(Wirless NIC).
무선 NIC는 Wi-Fi를 통해 무선 네트워크에 연결하는 카드입니다.
일반적으로 노트북, 태블릿, 스마트폰 같은 휴대용 장치에서 많이 사용되며, 이동성과 유연성이 장점입니다.
무선 네트워크 카드는 다양한 Wi-Fi 표준(802.11a/b/g/n/ac/ax)을 지원하며, 무선 엑세스 포인트(AP, Access Point)와 통신하여 인터넷에 연결됩니다.
3️⃣ 내장형 네트워크 카드.
오늘날 대부분의 컴퓨터, 노트북, 스마트폰 등에는 내장형 NIC가 장착되어 있어, 별도의 네트워크 카드를 설치하지 않아도 네트워크에 연결할 수 있습니다.
이러한 내장형 NIC는 메인보드에 직접 통합되어 있습니다.
4️⃣ 외장형 네트워크 카드.
USB 네트워크 어댑터와 같은 외장형 NIC는 USB 포트에 연결하여 사용됩니다.
이러한 외장형 네트워크 카드는 주로 내장 NIC가 없는 장치에서 네트워크 기능을 확장하거나, 추가 네트워크 인터페이스가 필요할 때 사용됩니다.
4️⃣ 네트워크 인터페이스 카드의 동작 원리.
1️⃣ 데이터 패킷 전송.
NIC는 컴퓨터의 CPU나 메모리에서 데이터를 받아 이를 패킷으로 변환하고, 네트워크를 통해 전송합니다.
이때 데이터는 이더넷 케이블이나 Wi-Fi 신호를 통해 송신됩니다.
2️⃣ 데이터 패킷 수신.
NIC는 네트워크에서 전송된 데이터를 받아 컴퓨터가 처리할 수 있는 형식으로 변환하여 운영체제나 응용 프로그램에 전달합니다.
3️⃣ 네트워크 프로토콜 처리.
NIC는 다양한 네트워크 프로토콜(예: TCP/IP, UDP 등)을 처리합니다.
각 프로토콜에 따라 데이터를 올바르게 전송하고 수신하는 역할을 하며, 이를 통해 안정적인 네트워크 통신이 가능해집니다.
4️⃣ MAC 주소 기반 통신.
네트워크 상에서 데이터는 MAC 주소를 기반으로 각 장치에 전송됩니다.
NIC는 MAC 주소를 사용해 패킷이 올바른 목적지로 전달되도록 관리합니다.
예를 들어, 스위치나 라우터는 패킷의 MAC 주소를 확인하여 적절한 장치로 데이터를 전달합니다.
5️⃣ 네트워크 인터페이스 카드(NIC, Network Interface Card)의 중요성.
1️⃣ 네트워크 연결의 필수 장치.
NIC는 컴퓨터와 네트워크를 연결하는 가장 중요한 장치입니다.
NIC가 없으면 컴퓨터나 장치는 네트워크에 연결될 수 없으며, 인터넷 접속이나 로컬 네트워크에서의 파일 공유, 자원 공유 등이 불가능합니다.
2️⃣ 고속 데이터 전송.
NIC는 고속으로 데이터를 송수신할 수 있게 해주며, 특히 유선 네트워크에서는 1Gbps 이상의 속도를 제공하여 대용량 파일 전송이나 고속 인터넷 연결에 유리합니다.
3️⃣ 다양한 네트워크 환경 지원.
NIC는 유선과 무선 네트워크 모두를 지원하며, 이를 통해 다양한 네트워크 환경에서 장치들이 자유롭게 연결되고 통신할 수 있습니다.
4️⃣ 네트워크 보안.
NIC는 네트워크에서 보안 통신을 가능하게 하며, 데이터 암호화 및 인증을 처리하는 데 중요한 역할을 합니다.
이를 통해 데이터가 안전하게 송수신될 수 있습니다.
6️⃣ 결론.
NIC는 컴퓨터나 네트워크 장치가 네트워크에 연결하여 데이터를 송수신할 수 있도록 해주는 필수적인 하드웨어입니다.
NIC는 네트워크 연결을 제공하고, 데이터를 패킷 단위로 변환하여 네트워크 상의 다른 장치와 통신할 수 있도록 도와줍니다.
유선과 무선 NIC가 있으며, 오늘날 대부분의 장치에는 NIC가 내장형으로 포함되어 있지만, 추가적인 필요에 따라 외장형 NIC를 사용할 수도 있습니다.
-
🌐[Network] MAC(Media Access Control) 주소란 무엇일까요?
🌐[Network] MAC(Media Access Control) 주소란 무엇일까요?
MAC 주소(Media Access Control Address)는 네트워크 인터페이스 카드(NIC, Network Interface Card)에 할당된 고유한 식별자로, LAN(Local Area Network)과 같은 네트워크에서 장치들이 서로 통신할 때 사용됩니다.
MAC 주소는 네트워크 장치의 물리적인 하드웨어 주소이며, 네트워크에서 각 장치를 구분하는 데 사용됩니다.
MAC 주소는 이더넷과 Wi-Fi와 같은 여러 네트워크 기술에서 사용됩니다.
🙋♂️ 네트워크 인터페이스 카드(NIC, Network Interface Card)란 무엇일까요?
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
1️⃣ MAC 주소의 주요 특징.
1️⃣ 고유성.
MAC 주소는 네트워크 카드 제조사에서 전 세계적으로 유일하게 할당되며, 각 네트워크 장치(컴퓨터, 프린터, 스마트폰 등)에 고유하게 부여됩니다.
즉, 각 장치의 네트워크 카드(NIC, Network Interface Card)마다 하나의 고유한 MAC 주소가 존재합니다.
2️⃣ 48비트 주소.
MAC 주소는 48비트(6바이트) 크기의 이진수로 표현되며, 보통 16진수로 표기됩니다.
48비트는 총 281조 개의 고유한 MAC 주소를 생성할 수 있게 합니다.
예: 00:1A:2B:3C:4D:5E 또는 00-1A-2B-3C-4D-5E
3️⃣ 하드웨어 주소.
MAC 주소는 네트워크 인터페이스 카드(NIC, Network Interface Card) 자체에서 영구적으로 할당된 주소로, 장치가 네트워크에 물리적으로 연결될 때 사용됩니다.
IP 주소는 네트워크 내에서 동적으로 할당될 수 있지만, MAC 주소는 장치의 하드웨어에 고정되어 있습니다.
4️⃣ 계층 2 주소.
MAC 주소 OSI 7계층 모델의 데이터 링크 계층(Layer 2)에서 사용됩니다.
즉, MAC 주소는 같은 네트워크(LAN) 내에서 장치 간 데이터를 전달하는 데 사용됩니다.
네트워크의 라우터나 스위치는 MAC 주소를 사용하여 데이터를 적절한 장치로 전달합니다.
🙋♂️ OSI 7계층 모델
🙋♂️ OSI 7계층 모델 - 계층별 기능
2️⃣ MAC 주소의 구조.
MAC 주소는 두 부분으로 나누어집니다.
1️⃣ 제조사 식별자(OUI, Organizationally Unique Identifier)
MAC 주소의 첫 24비트(앞의 6자리 16진수)는 네트워크 카드 제조사를 나타냅니다.
이 부분은 OUI라고 하며, 각 네트워크 카드 제조사는 고유한 OUI를 할당받습니다.
예: 00:1A:2B -> 제조사의 OUI
2️⃣ 장치 식별자(Device Identifier)
MAC 주소의 나머지 24비트(뒤의 6자리 16진수)는 제조사가 해당 네트워크 카드나 장치에 부여한 고유 식별자입니다.
예: 3C:4D:5E -> 장치의 고유 식별자
3️⃣ 결합.
이 두 부분(OUI + Device Identifier)을 결합하여 하나의 고유한 MAC 주소가 만들어집니다.
예: 00:1A:2B:3C:4D:5E
3️⃣ MAC 주소의 역할.
1️⃣ 로컬 네트워크(LAN)에서 데이터 전송.
MAC 주소는 LAN(Local Area Network) 내에서 데이터 프레임을 전달할 때 사용됩니다.
네트워크 장치가 데이터를 보낼 때, 해당 데이터는 송신자의 MAC 주소를 포함하고, 스위치와 같은 네트워크 장치가 이를 읽어 적절한 목적지로 데이터를 전달합니다.
2️⃣ 데이터 링크 계층에서 통신.
MAC 주소는 데이터 링크 계층에서 장치 간 프레임을 전달하는 데 사용됩니다.
이더넷 네트워크에서 MAC 주소는 같은 네트워크 내의 장치 간에 데이터를 정확하게 전달하기 위한 핵심 역할을 합니다.
3️⃣ ARP(Address Resolution Protocol)와의 연관성.
ARP는 IP 주소와 MAC 주소를 연결해주는 프로토콜입니다.
네트워크에서 IP 주소를 통해 데이터를 전송 할 때, ARP는 해당 IP 주소에 대한 MAC 주소를 찾아내어, 데이터를 올바른 장치로 전달할 수 있게 합니다.
이는 특히 로컬 네트워크에서 중요한 역할을 합니다.
4️⃣ 네트워크 장치의 고유 식별.
MAC 주소는 각 장치의 고유한 하드웨어 주소이므로, 네트워크에서 장치를 고유하게 식별하는 데 사용됩니다.
네트워크 관리자들은 MAC 주소를 이용해 네트워크에 연결된 장치들을 추적하거나, 특정 장치에 대해 네트워크 접근을 제어할 수 있습니다.
4️⃣ MAC 주소와 IP 주소의 차이점.
MAC 주소는 하드웨어 주소로, 네트워크 인터페이스 카드(NIC, Network Interface Card)에 고유하게 할당된 고정된 주소입니다.
장치가 네트워크에 물리적으로 연결되면 MAC 주소를 통해 통신하게 됩니다.
IP 주소는 논리주소로, 네트워크 장치가 인터넷 프로토콜을 통해 통신할 때 사용됩니다.
IP 주소는 동적으로 할당되거나 수동으로 설정할 수 있으며, 네트워크 간에 데이터를 전송할 때 사용됩니다.
5️⃣ MAC 주소의 예시.
1️⃣ 이더넷 프레임에서의 사용.
네트워크에서 컴퓨터 A가 컴퓨터 B로 데이터를 전송하려고 할 때, 이더넷 프레임에는 송신자의 MAC 주소와 수신자의 MAC 주소가 포함됩니다.
이때, 네트워크 장비(예: 스위치)는 수신자의 MAC 주소를 기반으로 데이터를 올바른 장치(B)로 전달합니다.
[출발지 MAC 주소] -> [목적지 MAC 주소]
00:1A:2B:3C:4D:5E -> 00:1A:7C:8D:9F:0A
2️⃣ ARP(Address Resolution Protocol)를 통한 IP-MAC 매핑.
네트워크에서 컴퓨터가 IP 주소를 통해 데이터를 전송할 때, ARP 프로토콜이 IP 주소에 대응하는 MAC 주소를 찾아 프레임을 생성하고, 데이터를 전송합니다.
예를 들어, 컴퓨터 A(192.168.1.2)가 컴퓨터 B(192.168.1.3)로 데이터를 전송 할 때 A는 ARP를 사용해 컴퓨터 B의 MAC 주소를 찾아내어 데이터를 전송합니다.
6️⃣ MAC 주소의 보안 이슈.
1️⃣ MAC 주소 스푸핑(Spoofing)
MAC 주소는 네트워크에서 장치를 식별하는 데 사용되지만, MAC 주소 스푸핑이라는 공격을 통해 MAC 주소를 위조할 수 있습니다.
공격자는 자신의 장치의 MAC 주소를 다른 장치의 MAC 주소로 변경하여 네트워크에서 허가되지 않은 접근을 시도할 수 있습니다.
2️⃣ MAC 주소 필터링.
네트워크 보안을 강화하기 위해 일부 네트워크 관리자들은 MAC 주소 필터링을 사용하여 허용된 장치들만 네트워크에 접속할 수 있도록 설정합니다.
그러나 MAC 주소가 스푸핑될 수 있기 때문에, 보안 측면에서 완벽한 방법은 아닙니다.
7️⃣ 결론.
MAC 주소는 네트워크 장치에 고유하게 할당된 물리적 하드웨어 주소로, 로컬 네트워크에서 데이터 전송 및 장치 식별에 중요한 역할을 합니다.
MAC 주소는 OSI 모델의 데이터 링크 계층(Layer 2)에서 사용되며, 이더넷과 같은 네트워크 기술에서 장치 간 통신을 가능하게 합니다.
MAC 주소는 고유하며 변경되지 않는 반면, IP 주소는 논리적이고 변경될 수 있습니다.
MAC 주소는 보안과 통신에서 중요한 역할을 하지만, 스푸핑 등의 보안 위협도 존재할 수 있습니다.
-
🌐[Network] 이더넷(Ethernet)이란 무엇일까요?
🌐[Network] 이더넷(Ethernet)이란 무엇일까요?
이더넷(Ethernet) 은 컴퓨터 네트워크 기술 중 하나로, LAN(Local Area Network, 근거리 통신망) 에서 주로 사용됩니다.
이더넷은 컴퓨터와 네트워크 장치들을 유선으로 연결하여 데이터를 송수신할 수 있도록 하는 표준 프로토콜입니다.
이더넷은 전 세계적으로 가장 널리 사용되는 LAN 기술이며, 빠르고 안정적인 데이터 전송을 제공합니다.
🙋♂️ LAN(Local Area Network)이란 무엇일까요?
1️⃣ 이더넷의 주요 특징.
1️⃣ 유선 네트워크 기술.
이더넷은 주로 이더넷 케이블(UTP, CAT5e, CAT6 등) 을 사용하여 네트워크 장치들을 물리적으로 연결합니다.
각 장치는 네트워크 인터페이스 카드(NIC, Network Interface Card) 를 통해 이더넷 네트워크에 연결됩니다.
2️⃣ 고속 데이터 전송.
이더넷은 고속 데이터 전송을 지원합니다.
초기 이더넷은 10Mbps 속도를 제공했지만, 현재는 1Gbps(1기가바이트 이더넷), 10Gbps 또는 그 이상의 속도를 제공하는 고속 이더넷이 널리 사용됩니다.
3️⃣ 통신 방식.
이더넷은 패킷 교환 방식을 사용하여 데이터를 전송합니다.
데이터를 작은 패킷으로 분할하여 전송하며, 각 패킷은 송신지와 목적지의 MAC 주소를 포함하여 네트워크 장치 간에 이동합니다.
CSMA/CD(Carrier Sense Multiple Access with Collision Detection) 기술을 사용하여, 네트워크 상에서 데이터 충돌을 감지하고 처리합니다.(현대의 이더넷은 스위치 기반이므로 충돌이 거의 발생하지 않음.)
4️⃣ 이더넷 스위치.
현대의 이더넷 네트워크는 스위치를 사용하여 데이터를 효율적으로 전송합니다.
스위치는 네트워크에 연결된 장치들 간의 데이터 트래픽을 관리하고 데이터를 목적지 MAC 주소에 따라 적절한 장치로 전달합니다.
5️⃣ 표준화.
이더넷은 IEEE 802.3이라는 국제 표준에 의해 정의된 네트워크 기술입니다.
이 표준은 물리적 연결 방식, 데이터 전송 방식, 신호 처리 등을 규정하여, 전 세계에서 호환 가능한 네트워크를 구축할 수 있도록 합니다.
2️⃣ 이더넷의 주요 구성 요소.
1️⃣ 이더넷 케이블.
이더넷은 유선 네트워크이기 때문에 이더넷 케이블을 통해 장치들을 연결합니다.
일반적으로 UTP(Unshielded Twisted Pair) 케이블이 사용되며, CAT5e, CAT6 등의 케이블이 데이터 전송 속도에 따라 선택됩니다.
CAT5e : 1Gbps 속도를 지원하는 케이블.
CAT6 : 10Gbps까지 지원하는 케이블.
2️⃣ 네트워크 인터페이스 카드(NIC, Network Interface Card).
이더넷에 연결된 각 장치(컴퓨터, 프린터 등)는 네트워크 인터페이스 카드(NIC, Network Interface Card) 를 통해 이더넷 네트워크와 통신합니다.
NIC는 데이터를 송수신하고, 장치 간의 통신을 처리하는 하드웨어입니다.
3️⃣ 스위치(Switch).
이더넷 스위치는 네트워크의 중앙에서 여러 장치를 연결하고, 각 장치가 보낸 데이터를 올바른 목적지로 전달하는 역할을 합니다.
스위치는 각 장치의 MAC 주소를 기억하고, 데이터를 적절한 포트로 전달함으로써 네트워크 효율을 높입니다.
4️⃣ 라우터(Router).
이더넷 네트워크에서 라우터는 네트워크 외부(예: 인터넷)와 통신할 수 있도록 연결을 제공합니다.
라우터는 이더넷 네트워크와 외부 네트워크 간의 데이터를 주고받는 역할을 합니다.
3️⃣ 이더넷의 작동 원리.
1️⃣ 패킷 전송.
이더넷은 데이터를 패킷이라는 작은 단위로 분할하여 전송합니다.
각 패킷은 데이터를 포함하며, 출발지 MAC 주소와 목적지 MAC 주소 등의 정보를 포함합니다.
2️⃣ 스위치를 통한 전달.
네트워크에 연결된 장치들이 스위치를 통해 데이터를 주고받습니다.
스위치는 장치들의 MAC 주소를 기억하고, 데이터를 목적지 장치로 정확하게 전달합니다.
이를 통해 네트워크 효율성이 증가하고, 충돌이 최소화됩니다.
3️⃣ 데이터 충돌 방지.
이더넷은 CSMA/CD(Carrier Sense Multiple Access with Collision Detection) 라는 기술을 사용하여 데이터 충돌을 감지하고, 충돌이 발생하면 데이터를 재전송하는 방식으로 동작합니다.
그러나 현대의 스위치 기반 이더넷에서는 충돌이 거의 발생하지 않기 때문에 이 메커니즘은 잘 사용되지 않습니다.
3️⃣ 이더넷의 유형.
1️⃣ 10Base-T (초기 이더넷)
초기의 이더넷 표준으로, 10Mbps 속도를 제공하며, 주로 구리 케이블을 사용하여 연결됩니다.
오늘날에는 거의 사용되지 않으며, 더 빠른 이더넷 기술로 대체되었습니다.
2️⃣ Fast Ethernet
100Base-TX라고도 불리며, 100Mbps 속도를 제공하는 이더넷입니다.
이전의 10Mbps 이더넷보다 빠른 데이터 전송을 지원하며, 일부 구현 네트워크 환경에서 여전히 사용됩니다.
3️⃣ Gigabit Ethernet
10 Gigabit Ethernet
10Gbps 속도를 제공하는 고속 이더넷으로, CAT6 이상의 고성능 케이블을 사용하여 연결됩니다.
데이터 센터, 서버, 고속 네트워크 환경에서 주로 사용됩니다.
4️⃣ PoE(Power over Ethernet)
PoE는 이더넷 케이블을 통해 전력과 데이터를 동시에 전송할 수 있는 기술입니다.
주로 IP 카메라, 무선 엑세스 포인트(AP), VoIP 전화기 등에서 사용되며, 별도의 전원 케이블 없이 네트워크 장치에 전력을 공급할 수 있습니다.
4️⃣ 이더넷의 장점.
1️⃣ 빠르고 안정적.
이더넷은 유선 네트워크로, 매우 빠르고 안정적인 데이터 전송을 제공합니다.
특히 Gigabit Ethernet 및 10Gbps 이더넷은 고속 네트워크 환경에서 매우 유리합니다.
2️⃣ 보안성.
이더넷은 유선으로 연결되기 때문에 무선 네트워크(Wi-Fi) 에 비해 보안 위협이 적습니다.
외부에서 물리적으로 접근하지 않는 한 네트워크 통신을 가로채는 것이 어렵습니다.
3️⃣ 네트워크 혼잡 감소.
스위치 기반 이더넷에서는 각 장치 간의 통신 경로가 충돌 없이 독립적으로 처리되므로, 네트워크 혼잡이 적고 데이터 전송 속도가 빠릅니다.
4️⃣ 비용 효율적.
이더넷은 전 세계에서 표준화된 기술로, 저렴한 비용으로 네트워크를 구축할 수 있습니다.
이더넷 장비와 케이블은 널리 사용되기 때문에 비용이 낮고 쉽게 구할 수 있습니다.
5️⃣ 이더넷의 단점.
1️⃣ 이동성 부족.
이더넷은 유선 네트워크이기 때문에, 사용자가 네트워크에 물리적으로 연결된 상태에서만 통신이 가능합니다.
무선 네트워크(Wi-Fi) 와 달리 자유롭게 이동하며 사용할 수 없습니다.
2️⃣ 배선 복잡성.
여러 장치를 연결해야 하는 대규모 네트워크에서는 많은 이더넷 케이블이 필요하며, 이로 인해 물리적 배선이 복잡해질 수 있습니다.
케이블 관리가 어려울 수 있습니다.
5️⃣ 결론.
이더넷(Ethernet) 은 LAN(Local Area Network)에서 유선으로 데이터를 송수신하는데 사용되는 대표적인 네트워크 기술입니다.
이더넷은 고속, 안정성, 보안성에서 뛰어나며, 전 세계적으로 가장 널리 사용되는 네트워크 표준입니다.
현대의 고속 이더넷 기술(1Gbps, 10Gbps)은 빠르고 효율적인 네트워크 통신을 제공하며, 특히 대규모 데이터 센트와 기업 네트워크에서 중요한 역할을 합니다.
-
💾 [CS] API(Application Programming Interface)란 무엇일까?
“💾 [CS] API(Application Programming Interface)란 무엇일까?”
API(Application Programming Interface) 는 애플리케이션이 다른 소프트웨어나 서비스와 상호작용할 수 있도록 하는 인터페이스를 의미합니다.
쉽게 말하면, API는 프로그램 간의 통신을 가능하게 해주는 일종의 계약 또는 약속입니다.
API는 개발자가 특정 기능을 사용할 수 있게 미리 정의된 메서드와 규칙의 집합을 제공합니다.
1️⃣ API의 기본 개념.
인터페이스
API는 두 시스템 간의 상호작용을 정의하는 경계 역할을 합니다.
이를 통해 두 시스템이 서로 데이터를 주고받거나 기능을 요청할 수 있습니다.
통신
API는 소프트웨어 애플리케이션이 다른 소프트웨어 서비스나 라이브러리와 통신하는 방법을 제공합니다.
예를 들어, API를 통해 애플리케이션은 외부 서비스에서 데이터를 가져오거나, 데이터베이스에 저장하는 등의 작업을 할 수 있습니다.
추상화
API는 내부적인 구현을 숨기고, 외부 개발자가 필요한 기능만 사용할 수 있게 추상화된 기능을 제공합니다.
내부의 복잡한 로직을 신경 쓰지 않고, 정의된 명령만 호출하면 해당 기능을 사용할 수 있습니다.
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.
2️⃣ API의 유형.
1️⃣ 웹 API
웹에서 동작하는 API로, HTTP 프로토콜을 통해 애플리케이션 간 통신을 처리합니다.
예를 들어, RESTful API나 SOAP API는 웹을 통해 데이터를 주고받는 데 사용됩니다.
웹 API는 클라이언트와 서버 간의 통신을 쉽게 해주며, 웹 서비스 또는 클라우드 서비스에서 주로 사용됩니다.
예: GET, POST, PUT, DELETE 메서드를 사용하여 서버와 데이터를 주고받는 API.
2️⃣ 라이브러리 API
프로그래밍 언어에서 제공하는 라이브러리의 기능을 사용할 수 있게 해주는 API입니다.
특정 기능이나 데이터 구조를 제공하여 개발자가 해당 기능을 구현할 수 있도록 도와줍니다.
예: Java의 java.util.List API는 목록 데이터를 쉽게 다룰 수 있는 메서드와 클래스를 제공합니다.
3️⃣ 운영체제 API
운영체제가 제공하는 시스템 자원을 애플리케이션이 사용할 수 있도록 해주는 API입니다.
파일 시스템, 네트워크, 하드웨어 장치와의 상호작용을 처리할 수 있게 합니다.
예: Windows API는 파일 읽기/쓰기, 네트워크 통신 등을 위한 다양한 기능을 제공합니다.
4️⃣ 프레임워크 API
특정 프레임워크에서 제공하는 API는 해당 프레임워크의 기능을 쉽게 사용할 수 있도록 도와줍니다.
예를 들어, Spring 프레임워크에서 제공하는 API는 웹 애플리케이션 개발에 필요한 다양한 기능을 제공합니다.
🙋♂️ 라이브러리(Library)와 프레임워크(Framework)의 차이점.
3️⃣ API의 동작 원리.
API는 주로 요청(Request)과 응답(Response)으로 작동합니다.
클라이언트가 특정 작업을 요청하면, 서버가 해당 작업을 처리한 후 결과를 반환하는 방식입니다.
1. 클라이언트가 요청 : 클라이언트 애플리케이션은 API를 사용해 특정 작업을 서버에 요청합니다.
2. 서버가 처리 : API 서버는 요청을 처리하고, 필요한 데이터를 조회하거나 작업을 수행합니다.
3. 서버가 응답 : 서버는 요청에 대한 결과(데이터나 작업 성공 여부)를 클라이언트에게 응답으로 반환합니다.
4️⃣ API의 예시.
1️⃣ Google Maps API.
개발자는 Google Maps API를 사용해 자신의 애플리케이션에 지도 기능을 추가할 수 있습니다.
이를 통해 사용자는 실시간 지도 정보를 보고, 길 안내를 받을 수 있습니다.
예: https://maps.googleapis.com/maps/api/geocode/json?address=New+York
2️⃣ (현)X (구)Twitter API.
(현)X (구)Twitter는 API를 통해 외부 애플리케이션이 트윗을 읽거나 작성할 수 있는 기능을 제공합니다.
예: 특정 해시태그로 트윗 검색, 사용자 프로필 정보 조회 등.
3️⃣ JPA API
JAP(Java Persistence API)는 자바 애플리케이션에서 데이터베이스와 상호작용할 수 있는 API로, 객체지향적으로 데이터베이스 작업을 할 수 있게 합니다.
개발자는 SQL을 직접 작성하지 않고, JPA가 제공하는 메서드를 통해 데이터베이스 작업을 처리할 수 있습니다.
EntityManager em = entityManagerFactory.createEntityManager();
User user = em.find(User.class, 1L) // User 객체 조회
🙋♂️ 영속성(Persistence)란 무엇일까?
4️⃣ API의 단점.
1️⃣ 의존성.
외부 API를 사용할 경우, 해당 API에 의존하게 됩니다.
만약 API가 변경되거나 중단되면 애플리케이션이 영향을 받을 수 있습니다.
🙋♂️ 의존성(Dependency)
2️⃣ 성능.
API를 사용한 통신이 느릴 수 있습니다.
특히 원격 서버와의 API 호출은 네트워크 지연에 영향을 받을 수 있습니다.
3️⃣ 제한 사항.
API 제공자는 사용량을 제한할 수 있습니다.
예를 들어, 하루에 호출할 수 있는 API 요청 수에 제한이 있을 수 있습니다.
5️⃣ 결론.
API는 소프트웨어 애플리케이션이 서로 상호작용할 수 있도록하는 중간 다리 역할을 합니다.
이를 통해 개발자는 다른 소프트웨어의 기능을 재사용하거나, 외부 서비스와의 통신을 통해 더 풍부한 애플리케이션을 만들 수 있습니다.
API는 현대 소프트웨어 개발에서 필수적인 구성 요소로 자리 잡고 있으며, 이를 통해 애플리케이션 간 통합과 협업이 더 쉽게 이루어집니다.
-
🍃[Spring] JPA를 사용하는 이유.
🍃[Spring] JPA를 사용하는 이유.
JPA(Java Persistence API) 를 사용하는 이유는 주로 데이터베이스와 객체 간의 상호작용을 효율적으로 처리하고, 객체지향 프로그래밍(OOP) 을 데이터베이스에 쉽게 적용하기 위해서 입니다.
JPA는 데이터베이스 작업을 단순화하고, 개발자가 데이터베이스에 의존적인 SQL 쿼리를 직접 작성하는 대신, 객체 모들을 통해 데이터를 관리할 수 있게 해줍니다.
🙋♂️ 소프트웨어 공학에서의 모듈
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
1️⃣ JPA를 사용하는 주요 이유.
1. 객체-관계 매핑(ORM, Object-Relational Mapping) 지원.
JPA는 ORM(Object-Relational Mapping) 표준을 제공합니다.
ORM을 사용하면 객체와 데이터베이스 테이블 간의 변환을 자동화할 수 있습니다.
즉, 데이터베이스의 행(row)을 객체로 매핑하고, 객체의 속성을 데이터베이스의 열(column)과 연결합니다.
이를 통해 개발자는 SQL 쿼리 작성에 신경 쓰지 않고, 객체 지향적으로 데이터를 처리할 수 있습니다.
2. 데이터베이스 독립성.
JPA는 데이터베이스 밴더에 종속적이지 않은 추상화 계층을 제공합니다.
JPA를 사용하면 애플리케이션에서 특정 데이터베이스에 의존하지 않으므로, 데이터베이스를 교체하거나 다중 데이터베이스를 사용할 때도 애플리케이션 코드를 크게 변경할 필요가 없습니다.
예를 들어, MySQL에서 PostgreSQL로 전환해도 JPA의 설정만 변경하면 대부분의 코드를 수정하지 않고 사용할 수 있습니다.
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.
3. SQL 작성 부담 감소.
JPA는 데이터를 조회, 저장, 수정, 삭제하는 데 필요한 SQL 쿼리를 자동으로 생성해줍니다.
개발자가 직접 SQL을 작성하지 않아도 되고, 객체 지향적으로 데이터 처리를 할 수 있습니다.
이를 통해 개발자는 비즈니스 로직에 집중할 수 있으며, 복잡한 SQL 작성의 부담을 줄일 수 있습니다.
4. 데이터베이스 스키마 관리.
JPA는 데이터베이스 스키마를 자동으로 생성하거나 업데이트하는 기능을 제공합니다.
이를 통해 개발자는 애플리케이션 코드만으로 데이터베이스 테이블을 자동으로 생성하거나 갱신할 수 있습니다.
애플리케이션이 시작될 때 데이터베이스 테이블을 자동으로 생성하고, 새로운 엔티티(객체)를 추가하면 그에 따라 데이터베이스 스키마를 자동으로 업데이트할 수 있습니다.
5. 캐싱 지원.
JPA는 1차 캐시와 2차 캐시를 제공하여 성능을 최적화할 수 있습니다.
1차 캐시는 영속성 컨텍스트(Entity Manager)가 관리하는 캐시로, 동일한 트랜잭션 내에서 이미 조회된 엔티티는 다시 데이터베이스에서 조회하지 않고 캐시에서 가져옵니다.
2차 캐시는 애플리케이션 전체에서 공유되는 캐시로, 애플리케이션 성능을 높이는 데 도움을 줍니다.
6. 트랜잭션 관리.
JPA는 트랜잭션 관리를 단순화합니다.
JPA는 데이터베이스 작업을 트랜잭션으로 묶어주기 때문에, 트랜잭션이 완료되면 모든 변경 사항이 원자적으로 데이터베이스에 반영되거나 롤백됩니다.
이를 통해 데이터 일관성과 무결성을 보장할 수 있습니다.
7. Lazy Loading과 Eager Loading.
JPA는 연관된 엔티티를 지연 로딩(Lazy Loading) 하거나 즉시 로딩(Eager Loading) 할 수 있습니다.
Lazy Loading은 연관된 데이터를 필요할 때만 가져오고, Eager Loading은 즉시 모든 연관 데이터를 한꺼번에 가져옵니다.
이를 통해 애플리케이션 성능을 최적화할 수 있습니다.
8. JPQL(Java Persistence Query Language).
JPA는 JPQL(Java Persistence Query Language) 이라는 객체 지향 언어를 제공합니다.
JPQL은 데이터베이스 테이블 대신 엔티티 객체 를 대상으로 쿼리를 작성할 수 있게 해줍니다.
이를 통해 SQL처럼 데이터베이스 종속적인 쿼리 대신, 객체를 대상으로 한 쿼리를 작성할 수 있습니다.
예를 들어, SQL이 아닌 객체 기반으로 작성된 JPQL 쿼리
SELECT u FROM User u WHERE u.age > 20
위 쿼리는 User 객체를 대상으로 하며, 데이터베이스에 의존하지 않습니다.
9. 유연한 관계 매핑.
JPA는 다양한 데이터베이스 관계를 객체로 매핑하는 방법을 지원합니다.
예를 들어, 1대1, 1대다, 다대일, 다대다 관계를 객체 지향적으로 표현할 수 있으며, 외래 키(foreign key)와 같은 데이터베이스 관계를 객체 간의 참조로 자연스럽게 처리할 수 있습니다.
10. 표준화된 API.
JPA는 자바 EE의 표준 API이기 때문에, 여러 구현체들(Hibernate, EclipseLink 등)에서 동일한 API로 데이터베이스 작업을 처리할 수 있습니다.
이는 개발자들이 특정 구현체에 종속되지 않고 코드를 작성할 수 있도록 해줍니다.
2️⃣ 결론.
JPA를 사용하면 객체 지향적으로 데이터베이스와 상호작용할 수 있어, 개발자는 SQL 작성 및 데이터베이스 종속성을 줄이고, 비즈니스 로직에 집중할 수 있습니다.
또한 데이터베이스 독립성, 트랜잭션 관리, 캐싱과 같은 기능을 통해 애플리케이션 성능과 유지보수성을 높일 수 있습니다.
JPQL과 같은 쿼리 언어를 통해 객체 중심으로 데이터 조회 및 관리를 할 수 있으며, 다양한 데이터베이스 관계를 쉽게 매핑할 수 있는 장점도 제공합니다.
JPA는 추상화 계층을 제공하여 객체 지향 설계와 데이터베이스 간의 간극을 줄이고, 개발 생산성을 크게 높일 수 있는 도구입니다.
-
🍃[Spring] JPA란 무엇인가요?
🍃[Spring] JPA란 무엇인가요?
JPA(Java Persistence API) 는 자바 애플리케이션에서 관계형 데이터베이스와 상호작용하기 위한 ORM(Objcet-Relational Mapping) 표준 입니다.
JPA는 자바 객체와 데이터베이스 테이블 간의 매핑을 자동으로 처리하여, 개발자가 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있도록 도와줍니다.
JPA는 자바 EE(Enterprise Edition) 의 공식 스팩 중 하나이며, 여러 구현체(Hibernate, EclipseLink, OpenJPA 등)들이 JPA 표준을 따릅니다.
1️⃣ JPA의 주요 개념.
1. 객체-관계 매핑(ORM, Object-Relational Mapping).
JPA는 ORM(Object-Relational Mapping) 을 통해 자바 객체를 데이터베이스 테이블에 자동으로 매핑합니다.
이를 통해 자바 객체와 데이터베이스 간의 변환을 수동으로 처리할 필요 없이, 객체 지향적으로 데이터베이스 작업을 수행할 수 있습니다.
ORM은 개발자가 직접 SQL을 작성하지 않고도 자바 객체를 조작함으로써 데이터를 처리할 수 있도록 도와줍니다.
2. 영속성(Persistence).
JPA에서 영속성은 자바 객체를 데이터베이스에 저장하고 관리하는 과정을 의미합니다.
JPA는 엔티티(Entity)라는 객체를 사용하여 데이터를 관리하며, 엔티티 객체는 특정 데이터베이스 테이블의 행(row)에 매핑됩니다.
영속성 컨텍스트(Persistence Context) 는 엔티티 객체를 관리하는 공간으로, 객체의 상태를 관리하고 변경 사항을 데이터베이스에 반영합니다.
3. 엔티티(Entity).
JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 자바 클래스입니다.
엔티티 클래스는 데이터베이스의 테이블을 모델링하며, 각 인스턴스는 데이터베이스 테이블의 한 행(row)에 대응됩니다.
엔티티 클래스는 @Entity 어노테이션으로 선언되며, 테이블의 열(column)은 클래스의 필드로 매핑됩니다.
예시
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters, Setters, Constructors
}
4. JPQL(Java Persistence Query Language).
JPA는 JPQL이라는 쿼리 언어를 제공하며, 이를 통해 데이터베이스에 질의를 수행할 수 있습니다.
JPQL은 SQL과 유사하지만, SQL이 데이터베이스 테이블을 대상으로 하는 반면, JPQL은 객체를 대상으로 궈리를 수행합니다.
이를 통해 데이터베이스에 종속되지 않고 객체 중심으로 데이터 조작이 가능합니다.
예시
SELECT u FROM User u WHERE u.name = 'Kobe'
위 JPQL 쿼리는 User 엔티티 객체를 대상으로 데이터를 조회하는 쿼리입니다.
5. 트랜잭션 관리.
JPA는 트랜잭션을 관리하여 데이터베이스 작업의 일관성과 안전성을 보장합니다.
트랜잭션은 데이터베이스에서 일련의 작업이 모두 성공하거나 모두 실해하는 것을 보장하는 메커니즘으로, JPA는 이를 자동으로 관리해줍니다.
트랜잭션이 완료되면 JPA는 데이터베이스에 모든 변경 사항을 반영하며, 문제가 발생하면 롤백합니다.
🙋♂️ 트랜잭션의 의미와 역할
6. 영속성 유닛(Persistence Unit).
영속성 유닛은 JPA가 데이터베이스와 상호작용하기 위해 필요한 설정 정보(데이터베이스 URL, 사용자 정보, 드라이버 등)를 담고 있는 논리적 단위입니다.
JPA는 이 영속성 유닛을 통해 데이터베이스와 연결을 맺고, 필요한 작업을 수행합니다.
영속성 유닛은 persistence.xml 파일을 통해 설정되며, 데이터베이스와의 연결 정보가 포함됩니다.
7. 캐싱.
JPA는 성능 최적화를 위해 1차 캐싱(영속성 컨텍스트에서 관리)와 2차 캐시를 제공합니다.
1차 캐시는 트랜잭션 범위 내에서 동일한 엔티티를 조회할 때 다시 데이터베이스에 접근하지 않고 캐시된 값을 사용합니다.
2️⃣ JPA의 장점.
1. 데이터베이스 독립성.
JPA는 데이터베이스에 종속되지 않는 추상화 계층을 제공하여, 특정 데이터베이스 벤더에 의존하지 않고 코드를 작성할 수 있습니다.
이를 통해 애플리케이션 코드의 이식성과 유지보수성이 높아집니다.
2. SQL 작성의 부담 감소.
JPA는 데이터를 조작하는 데 필요한 SQL 쿼리를 자동으로 생성합니다.
개발자는 객체 지향적인 방식으로 데이터를 처리하며, 직접 SQL을 작성하는 부담이 줄어듭니다.
3. 객체 지향 프로그래밍과 데이터베이스의 간극 해소.
JPA는 자바의 객체 지향 모델과 관계형 데이터베이스 간의 불일치를 해결합니다.
자바 객체의 필드를 데이터베이스의 열(Column)과 매핑하고, 자바 객체의 상태 변화를 데이터베이스에 자동으로 반영합니다.
4. 트랜잭션 관리 자동화.
JPA는 트랜잭션 관리를 자동화하여 개발자가 트랜잭션 범위를 명시하지 않아도, 데이터베이스 작업의 일관성을 유지할 수 있도록 도와줍니다.
5. JPQL을 통한 객체 중심의 쿼리.
JPQL은 SQL 대신 객체 모델을 기반으로 하는 쿼리 언어로, SQL에 종속되지 않고 객체 지향적으로 데이터를 조회할 수 있게 해줍니다.
6. 캐싱을 통한 성능 향상.
JPA는 1차 캐시와 2차 캐시를 통해 데이터베이스 접근 횟수를 줄이고, 성능을 향상시킬 수 있습니다.
3️⃣ JPA 구현체.
JPA는 인터페이스와 규약만을 정의하는 표준이기 때문에, 실제로 동작하기 위해서는 구현체가 필요합니다.
대표적인 JPA 구현체로는 다음과 같은 ORM(Object-Relational Mapping) 프레임워크들이 있습니다.
Hibernate : 가장 널리 사용되는 JPA 구현체로, JPA의 기능을 포함하면서도 JPA 이상의 기능들을 제공합니다.
EclipseLink : JPA의 레퍼런스 구현체로, 자바 EE 환경에서 많이 사용됩니다.
OpenJPA : Apache에서 제공하는 JPA 구현체입니다.
4️⃣ 결론.
JPA는 자바 애플리케이션에서 데이터베이스와의 상호작용을 더 쉽게 만들기 위한 ORM(Object-Relational Mapping) 표준입니다.
JPA를 사용하면 객체 지향적인 방식으로 데이터베이스 작업을 수행할 수 있으며, 데이터베이스 독립적인 코드를 작성할 수 있습니다.
또한, JPA는 트랜잭션 관리, 캐싱, 데이터베이스 스키마 자동 생성 등의 기능을 통해 개발자에게 많은 편의성을 제공합니다.
JPA는 개발자가 객체 모델에 집중할 수 있게 하면서도 데이터베이스와의 상호작용을 효율적으로 처리할 수 있도록 도와줍니다.
-
🌐[Network] LAN(Local Area Network)이란 무엇일까요?
🌐[Network] LAN(Local Area Network)이란 무엇일까요?
LAN(Local Area Network, 근거리 통신망) 은 한정된 지역 내에서 장치들 간에 네트워크 연결을 제공하는 통신망을 말합니다.
LAN은 일반적으로 한 건물이나 가정, 사무실과 같은 작은 지리적 범위 내에서 컴퓨터, 프린터, 서버, 스마트 기기 등 여러 장치들을 연결하여 데이터 통신을 가능하게 합니다.
1️⃣ LAN의 주요 특징.
1. 제한된 지리적 범위.
LAN은 보통 한정된 지역(집, 사무실, 학교, 캠퍼스 등) 내에서 운영됩니다.
WAN(Wide Area Network)이나 MAN(Metropolitan Area Network)과 비교하면, 상대적으로 작은 범위에서 장치들을 연결합니다.
2. 고속 데이터 전송.
LAN은 일반적으로 고속 데이터 전송을 지원합니다.
이더넷 기반 LAN의 경우 100Mbps에서 1Gbps, 심지어 10Gbps 이상의 속도를 지원하는 네트워크 환경도 있습니다.
이는 인터넷보다 빠른 속도를 제공하여, 장치 간의 데이터 전송을 더 효율적으로 처리할 수 있습니다.
3. 저비용.
LAN은 제한된 범위 내에서 구축되기 때문에, 대규모 네트워크(예: WAN(Wide Area Network))에 비해 구축 비용이 상대적으로 저렴합니다.
네트워크 장비와 배선 설치 등이 간단하고 저렴하게 처리될 수 있습니다.
4. 데이터 및 자원 공유.
LAN을 통해 데이터와 자원(프린터, 파일 서버 등) 을 여러 장치와 사용자 간에 공유할 수 있습니다.
예를 들어, 한 사무실 내에서 LAN을 통해 여러 컴퓨터가 하나의 프린터를 공유하거나, 파일 서버에 저장된 데이터를 서로 접근하고 수정할 수 있습니다.
5. 네트워크 보안.
LAN은 로컬 네트워크이기 때문에, 외부로부터 침입을 방지하는 다양한 보안 메커니즘을 쉽게 적용할 수 있습니다.
예를 들어, 방화벽, VPN, 암호화 등을 통해 네트워크 보안을 강화할 수 있습니다.
2️⃣ LAN의 구성 요소.
1. 네트워크 장비.
LAN을 구성하는 데 필요한 네트워크 장비들은 다음과 같습니다.
스위치(Switch) : 네트워크에서 데이터를 올바른 장치로 전달하는 역할을 합니다. 각 장치를 스위치에 연결하면, 스위치는 데이터를 목적지 MAC 주소에 따라 적절한 장치로 전달합니다.
라우터(Router) : LAN을 외부 네트워크(예: 인터넷)와 연결하는 장치입니다. LAN 내에서 데이터를 전송할 때는 스위치가 주로 사용되지만, 외부 네트워크와의 통신을 위해서는 라우터가 필요합니다.
액세스 포인트(AP, Access Point) : 무선 장치를 네트워크에 연결할 수 있게 해주는 장치입니다. 유선 네트워크를 무선으로 확장하여, 무선 기기(노트북, 스마트폰 등)가 LAN에 연결될 수 있게합니다.
2. 네트워크 케이블.
이더넷 케이블은 유선 LAN에서 각 장치(컴퓨터, 스위치, 라우터)를 물리적으로 연결하는 데 사용됩니다.
일반적으로 CAT5e, CAT6 같은 케이블이 사용되며, 데이터 전송 속도에 따라 다르게 선택됩니다.
3. 네트워크 인터페이스 카드(NIC).
네트워크 카드 또는 네트워크 인터페이스 카드(NIC, Network Interface Card) 는 각 장치를 LAN에 연결하는 역할을 합니다.
NIC는 데이터를 전송할 수 있도록 컴퓨터에 네트워크 포트를 제공하며, 유성 또는 무선으로 LAN에 연결됩니다.
4. 장치들.
컴퓨터, 프린터, 서버, 스마트폰 등의 장치들이 LAN에 연결되어 서로 데이터를 주고받거나 자원을 공유할 수 있습니다.
3️⃣ LAN의 유형.
1. 유선 LAN(Ethernet LAN).
유선 LAN은 이더넷 케이블을 사용하여 각 장치를 물리적으로 연결하는 방식입니다.
일반적으로 스위치에 여러 장치를 연결하고, 스위치가 데이터를 올바른 목적지로 전송합니다.
유선 LAN은 신뢰성 있고 속도가 빠르며, 보안 면에서도 유리합니다.</a>
2. 무선 LAN(Wireless LAN, WLAN).
무선 LAN은 Wi-Fi를 통해 장치들이 무선으로 연결되는 네트워크입니다.
무선 엑세스 포인트(AP, Access Point)를 통해 스마트폰, 노트북 같은 무선 장치들이 네트워크에 접속할 수 있습니다.
무선 LAN은 이동성과 유연성이 뛰어나지만, 유선 LAN에 비해 상대적으로 속도가 느기고, 보안 위협이 더 클 수 있습니다.
3. 가상 LAN(Virtual LAN, VLAN)
VLAN은 물리적으로 같은 네트워크 내에 있는 장치들을 논리적으로 구분하여, 마치 서로 다른 네트워크처럼 동작하게 합니다.
예를 들어, 하나의 물리적 네트워크에서 부서별로 논리적으로 네트워크를 나누어, 서로 다른 트래픽을 분리하고 관리할 수 있습니다.
4️⃣ LAN의 장점.
1. 고속 데이터 전송.
LAN은 인터넷 연결보다 빠른 속도를 제공합니다.
특히 유선 LAN은 1Gbps 이상의 속도를 제공할 수 있어, 대용량 파일 전송이나 고성능 네트워크 작업에 적합합니다.
2. 자원 공유.
LAN을 통해 여러 사용자가 프린터, 파일 서버, 데이터베이스 등의 자원을 쉽게 공유할 수 있습니다.
3. 비용 절감.
자원 공유를 통해 비용을 절감할 수 있습니다.
예를 들어, 여러 컴퓨터가 하나의 프린터를 공유하면, 각 컴퓨터마다 프린터를 구입할 필요가 없습니다.
4. 보안.
LAN은 내부 네트워크이기 때문에, 외부 위협으로부터 보호하기가 상대적으로 쉽습니다.
내부 사용자만 네트워크에 접근할 수 있도록 설정할 수 있으며, 네트워크 보안을 강화할 수 있습니다.
5. 네트워크 관리 용이.
LAN은 소규모 네트워크이기 때문에, 네트워크 관리와 유지보수가 상대적으로 간단합니다.
네트워크 장애가 발생했을 때 신속하게 문제를 파악하고 해결할 수 있습니다.
5️⃣ LAN의 단점.
1. 물리적 범위 제한.
LAN은 지리적으로 제한된 범위 내에서만 사용 가능하며, 먼 거리에 있는 장치를 연결하는 데에는 적합하지 않습니다.
2. 무선 LAN 보안 문제.
무선 LAN은 상대적으로 보안 위협에 더 취약할 수 있습니다.
와이파이 신호는 외부로 유출될 수 있기 때문에 불법적인 접근을 방지하기 위한 추가적인 보안 조치가 필요합니다.
3. 유선 LAN의 배선 문제.
유선 LAN은 케이블 설치가 필요하기 때문에, 물리적인 공간이 복잡해질 수 있으며, 이동성이 제한될 수 있습니다.
6️⃣ LAN과 다른 네트워크 유형과의 비교.
1. WAN(Wide Area Network)
WAN은 광범위한 지리적 범위를 아우르는 네트워크입니다.
여러 지역에 거쳐있는 네트워크나 인터넷 같은 거대한 네트워크가 WAN에 해당합니다.
LAN은 제한된 지역 내에서만 작동하는 반면, WAN은 여러 도시, 국가 또는 전 세계적으로 연결할 수 있습니다.
2. MAN(Metropolitan Area Network)
MAN은 도시 수준에서 운영되는 네트워크로, LAN보다는 크고 WAN보다는 작은 규모의 네트워크입니다.
대학교 캠퍼스, 기업 본사 등이 도시 또는 캠퍼스 전역을 커버하는 네트워크로 MAN을 구축할 수 있습니다.
7️⃣ 결론.
LAN(Local Area Network) 은 제한된 지역 내에서 장치 간의 네트워크 연결을 제공하여 데이터 통신과 자원 공유를 가능하게 하는 네트워크입니다.
LAN은 고속 데이터 전송과 효율적인 자원 공유를 제공하며, 주로 가정, 사무실, 학교 등에서 사용됩니다.
유선 LAN과 무선 LAN이 있으며, 각각 장단점이 있습니다.
LAN은 현대 컴퓨팅 환경에서 필수적인 네트워크 구성 요소로, 내부 네트워크 관리와 데이터 통신을 쉽게 처리할 수 있게 합니다.
-
💾 [CS] 영속성(Persistence)란 무엇일까?
“💾 [CS] 영속성(Persistence)란 무엇일까?”
영속성(Persistence) 이란 데이터나 객체의 상태를 지속적으로 저장하여 프로그램이 종료되거나 시스템이 재부팅되더라도 그 상태가 유지되는 것을 의미합니다.
간단히 말하면, 영속성은 데이터를 영구적으로 저장하는 능력을 가리킵니다.
소프트웨어 시스템에서는 주로 데이터베이스, 파일 시스템, 영속 저장소와 같은 외부 저장소에 데이터를 저장하는 것을 영속성이라고 합니다.
1️⃣ 영속성의 예.
데이터베이스에 저장된 데이터
데이터베이스에 저장된 데이터는 시스템이 종료되거나 전원이 꺼져도 영구적으로 저장됩니다.
프로그램이 다시 실행될 때 이 데이터를 불러와서 사용할 수 있습니다.
파일 시스템에 저장된 데이터
파일 시스템에 저장된 파일도 시스템 재부팅 후에도 유지되며, 나중에 다시 사용할 수 있습니다.
2️⃣ 영속성의 중요성.
소프트웨어 시스템에서는 대부분의 데이터가 메모리(휘발성)에만 존재하면 프로그램이 종료되거나 장애가 발생할 때 데이터를 잃게 됩니다.
영속성을 사용하면 프로그램이 종료되더라도 중요한 데이터는 데이터베이스나 파일 시스템에 저장되므로 시스템이 다시 시작되었을 때 데이터를 복구하거나 이어서 사용할 수 있습니다.
3️⃣ Java 및 Spring에서의 영속성.
Java 환경, 특히 JPA(Java Persistence API) 를 사용하는 애플리케이션에서 영속성은 주로 데이터베이스와 자바 객체 간의 상태 유지를 의미합니다.
JPA는 자바 객체를 데이터베이스에 영속적으로 저장하고, 필요할 때 데이터를 조회하여 다시 객체로 변환하는 과정을 자동으로 처리합니다.
JAP에서의 주요 개념.
1. 영속성 컨텍스트(Persistence Context)
영속성 컨텍스트는 JPA에서 자바 객체(Entity)를 관리하는 일종의 캐시 메모리입니다.
엔티티가 영속성 컨텍스트에 포함되면, 해당 엔티티는 데이터베이스에 영속됩니다.
영속성 컨텍스트는 엔티티의 상태 변화를 감지하고, 그 변화가 트랜잭션이 끝날 때 데이터베이스에 반영되도록 관리합니다.
2. 엔티티(Entity)
JPA에서 엔티티는 데이터베이스의 테이블과 매핑되는 자바 객체입니다.
엔티티는 데이터를 영속적으로 유지하기 위한 수단으로, JPA는 이를 데이터베이스에 저장하고 다시 불러옵니다.
3. EntityManager
EntityManager는 JPA의 핵심 인테페이스로, 엔티티의 CRUD(생성, 조회, 수정, 삭제) 작업을 담당합니다.
EntityManager는 영속성 컨텍스트를 관리하며, 데이터베이스와의 상호작용을 처리합니다.
persist() 메서드를 통해 엔티티를 영속성 컨텍스트에 저장하고, 이를 데이터베이스에 반영할 수 있습니다.
4. 영속 상태(Persistent State)
JPA에서 엔티티 객체는 다음 세 가지 상태를 가집니다.
비영속(Transient) : 데이터베이스와 전혀 연결되지 않은 상태입니다.
영속(Persistent) : 엔티티가 영속성 컨텍스트에 포함되어 관리되고 있는 상태로, 데이터베이스에 기록되거나 기록될 준비가 되어 있는 상태입니다.
준영속(Detached) : 영속성 컨텍스트에 의해 관리되지 않지만 데이터베이스에는 여전히 존재하는 상태입니다.
영속 상태 전이 예시
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters, Setters, Constructors
}
// 영속성 컨텍스트와 EntityManager를 사용한 영속성 예시
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
// 1. 비영속 상태: User 객체는 아직 데이터베이스와 연결되지 않음
User user = new User();
user.setName("Kobe");
// 2. 영속 상태: persist()로 영속성 컨텍스트에 저장됨
entityManager.persist(user);
// 3. 트랜잭션이 완료되면, 데이터베이스에 저장됨
entityManager.getTransaction().commit();
entityManager.close();
설명.
비영속 상태 : new User()로 생성된 객체는 메모리에서만 존재하고, 아직 데이터베이스와 연결되지 않았습니다.
영속 상태 : entityManager.persist(user)를 호출하면 객체는 영속성 컨텍스트에 저장되고, 데이터베이스에 반영될 준비가 됩니다.
트랜잭션이 커밋되면, 영속성 컨텍스트에 있는 객체의 상태가 데이터베이스에 영구적으로 저장됩니다.
4️⃣ 정리.
영속성은 데이터를 영구적으로 저장하는 기능입니다.
주로 데이터베이스나 파일 시스템에 데이터를 저장하여 프로그램이 종료되더라도 데이터가 유지될 수 있게 합니다.
JPA에서 영속성은 객체(Entity)의 상태를 영속성 컨텍스트에서 관리하고, 이 상태가 데이터베이스와 연동되어 지속될 수 있게 하는 것을 의미합니다.
영속성 컨텍스트는 JPA에서 엔티티 객체의 상태를 관리하고, 데이터베이스와의 동기화를 처리하는 중요한 메커니즘입니다.
-
💾 [CS] DIP의 정의에서 말하는 '추상화된 것'이란 무엇일까?
“💾 [CS] DIP의 정의에서 말하는 ‘추상화된 것’이란 무엇일까?”
우리가 잘 알고있는 SOLID 원칙 중 DIP(의존성 역전 원칙, Dependency Inversion Principle) 정의는 다음과 같습니다.
“고수준 모듈은 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화 된 것에 의존해야 합니다.”
여기서 고수준 모듈은 “비즈니스 로직” 을 뜻하고, 저수준 모듈은 세부 구현을 뜻합니다.
그렇다면 “추상화된 것” 이란 어떤 의미일까요?
🙋♂️ SOLID 원칙
🙋♂️ 의존성
1️⃣ DIP의 정의에서 말하는 “추상화된 것”이란?
DIP(Dependency Inversion Principle, 의존성 역전 원칙) 의 정의에서 말하는 “추상화된 것” 은 인터페이스 또는 추상 클래스를 의미합니다.
이 원칙에서 “추상화된 것에 의존해야 한다”는 것은, 구체적인 구현 클래스가 아닌 인터페이스나 추상 클래스 같은 추상적인 계층을 통해 상호작용해야 한다는 의미입니다.
2️⃣ 구체적인 설명.
DIP(Dependency Inversion Principle, 의존성 역전 원칙) 의 기본 개념은 고수준 모듈(비즈니스 로직) 과 저수준 모듈(세부적인 구현) 간의 의존성을 추상화된 계층으로 뒤집는 것입니다.
여기서 말하는 “추상화된 것” 은 구현이 아닌 계약을 의미하며, 이 계약은 인터페이스나 추상 클래스를 통해 정의됩니다.
고수준 모듈 : 시스템의 상위 레벨에 위치하는 모듈로, 일반적으로 비즈니스 로직을 처리합니다.
저수준 모듈 : 구체적인 세부 사항이나 기술적인 구현을 담당하는 모듈입니다. 예를 들어, 데이터베이스 처리, 파일 시스템 작업들이 해당됩니다.
🙋♂️ 비즈니스 로직(Business Logic)이란?
🙋♂️ 소프트웨어 공학에서의 모듈
🙋♂️ 모듈과 컴포넌트를 레고 불록에 비유해보면?!
예시
먼저 DIP를 위반하는 코드 예시를 보겠습니다.
이 코드는 고수준 모듈이 저수준 모듈의 구체적인 구현에 직접적으로 의존하는 경우입니다.
class EmailService {
public void sendEmail(String message) {
// 이메일 전송 로직
System.out.println("Email sent: " + message);
}
}
class NotificationManager {
private EmailService emailService;
public NotificationManager() {
this.emailService = new EmailService(); // 저수준 모듈의 구체적 구현에 의존
}
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
이 코드에서는 NotificationManager(고수준 모듈)가 EmailService(저수준 모듈)에 직접 의존하고 있습니다.
만약 NotificationManager가 다른 알림 방법(예: SMS)을 사용하고 싶다면, EmailService와 같은 저수준 모듈을 수정하거나 교체해야 하므로 유연성이 떨어집니다.
DIP 적용 (추상화된 것에 의존)
DIP를 적용하면, 고수준 모듈과 저수준 모듈 모두 추상화된 계층(인터페이스나 추상 클래스) 에 의존하게 됩니다.
이렇게 하면 고수준 모듈은 저수준 모듈의 구체적인 구현에 의존하지 않게 되어, 더 유연하고 확장 가능한 코드가 됩니다.
// 추상화된 것: Interface 정의 (추상화된 계층)
interface NotificationService {
void sendNotification(String message);
}
// 저수준 모듈: 구체적인 구현체
class EmailService implements NotificationService {
public void sendNotification(String message) {
System.out.println("SMS sent: " + message);
}
}
// 저수준 모듈: 또 다른 구체적인 구현체
class SMSService implements NotificatioonService {
public void sendNotificatioon(String message) {
System.out.println("SMS sent: " + message);
}
}
// 고수준 모듈: 추상화된 인터페이스에 의존
class NotificationManager {
private NotificationService notificationService;
public NotificationManager(NotificationService notificationService) {
this.notificationService = notificationService // 추상화된 것에 의존
}
public void sendNotification(String message) {
notificationService.sendNotification(message);
}
}
설명.
추상화된 것
NotificationService 인터페이스는 추상화된 것입니다.
고수준 모듈(NotificationManager)은 이제 구체적인 EmailService나 SMSService에 의존하지 않고, NotificationService라는 추상화된 인터페이스에 의존합니다.
구체적인 구현
EmailService와 SMSService는 각각 NotificationService 인터페이스를 구현한 저수준 모듈입니다.
결과.
이렇게 하면 NotificatioonManager는 EmailService 또는 SMSService 중 어떤 구현체를 사용하든지 상관하지 않습니다.
다른 알림 서비스가 필요하다면, 단순히 NotificationService를 구현하는 새로운 클래스를 만들고, 이를 NotificationManager에 전달하면 됩니다.
즉, 고수준 모듈과 저수준 모듈이 모두 추상화된 인터페이스에 의존하게 되어, 코드의 유연성과 확장성이 크게 향상됩니다.
3️⃣ 결론.
DIP에서 말하는 “추상화된 것” 은 구체적인 구현체가 아닌 인터페이스 또는 추상 클래스를 의미합니다.
고수준 모듈과 저수준 모듈 모두 이 추상화된 계층에 의존함으로써, 각 모듈 간 결합을 줄이고 유연한 시스템을 구축할 수 있습니다.
-
💾 [CS] 소프트웨어 공학에서의 모듈.
💾 [CS] 소프트웨어 공학에서의 모듈.
소프트웨어 공학에서의 모듈은 재사용 가능하고 독립적인 단위로, 특정 기능을 수행하는 소프트웨어의 구성 요소를 의미합니다.
모듈은 다른 모듈과 함께 하나의 시스템을 구성하며, 주로 잘 정의된 인터페이스를 통해 상호작용합니다.
모듈화된 설계는 복잡한 소프트웨어 시스템을 관리하기 쉽게 하며, 유지보수성과 재사용성을 높이는 데 중요한 역할을 합니다.
1️⃣ 모듈의 주요 특성.
1. 독립성.
모듈은 시스템 내에서 다른 모듈과 독립적으로 동작할 수 있습니다.
각 모듈은 자신의 책임을 다하며, 내부 구현은 다른 모듈과 독립적으로 변경할 수 있습니다.
2. 인터페이스
모듈은 외부와 상호작용하기 위해 명확하게 정의되 인터페이스를 가지고 있습니다.
이 인터페이스를 통해 모듈 간의 데이터 교환이나 기능 호출이 이루어집니다.
내부 구현 세부 사항은 인터페이스 뒤에 숨겨져 있어, 모듈 간의 결합을 느슨하게 유지합니다.
3. 응집성.
모듈 내의 구성 요소는 밀접하게 연관된 작업을 수행해야 하며, 모듈의 기능은 하나의 책임이나 목적에 집중해야 합니다.
이를 고응집성이라고 하며, 모듈화된 시스템의 중요한 특성 중 하나입니다.
4. 느슨한 결합.
모듈 간의 의존성은 최소화되어야 하며, 이를 통해 시스템의 변경 가능성과 유연성을 높입니다.
모듈 간 결합이 느슨할수록 각 모듈을 독립적으로 개발하고 수정할 수 있습니다.
5. 재사용성.
잘 설계된 모듈은 재사용 가능해야 합니다.
한 번 개발된 모듈은 여러 다른 프로젝트나 시스템에서 동일하게 사용될 수 있어야 하며, 이를 통해 개발 비용과 시간이 절약됩니다.
2️⃣ 모듈과 컴포넌트의 차이.
모듈과 컴포넌트는 종종 유사한 의미로 사용되지만, 용어의 사용 문맥에 따라 약간의 차이가 있을 수 있습니다.
모듈은 시스템을 구성하는 논리적인 단위로, 주로 코드의 주조화와 기능적 단위를 나타냅니다.
컴포넌트는 더 구체적이고 물리적인 소프트웨어 단위로, 주로 재사용 가능한 실행 가능한 단위를 의미합니다.
예를 들어, 컴포넌트는 라이브러리, 서비스, UI요소 등의 형태로 나타날 수 있습니다.
3️⃣ 모듈의 예시.
1. 모듈화된 시스템 설계.
소프트웨어 시스쳄이 여러 모듈로 나누어질 수 있습니다.
예를 들어, 전자 상거래 애플리케이션에서 다음과 같은 모듈이 있을 수 있습니다.
주문 처리 모듈 : 주문 생성, 확인, 결제 처리 등과 관련된 기능을 담당합니다.
사용자 관리 모듈 : 사용자 등록, 로그인, 프로필 관리 등과 관련된 기능을 담당합니다.
재고 관리 모듈 : 상품의 재고를 추적하고 관리하는 기능을 담당합니다.
각 모듈은 독립적으로 동작하며, 시스템의 일부분으로 기능합니다.
모듈들은 서로 명확한 인터페이스를 통해 상호작용하며, 모듈 간의 결합을 최소화하여 시스템의 확장성과 유지보수성을 높입니다.
2. 프로그래밍 언어에서의 모듈
프로그래밍 언어에서도 모듈화의 개념이 사용됩니다.
모듈은 여러 소스 파일이나 네임스페이스로 분리되어, 각기 다른 기능을 담고 있습니다.
예시.
디랙토리 구조
src/
├── com/
│ └── example/
│ ├── Main.java
│ ├── operations/
│ │ ├── MathOperations.java
│ │ └── MathOperationsImpl.java
│ └── interfaces/
│ └── MathOperations.java
MathOperations.java(인터페이스)
먼저, 수학 연산 모듈을 추상화하여 인터페이스로 정의합니다.
이렇게 하면 다양한 구현체를 사용할 수 있으며, 모듈간 결합을 줄일 수 있습니다.
```java
// com/example/interfaces/MathOperations.java
package com.example.interfaces;
public interface MathOperations {
int add(int a, int b);
int subtract(int a, int b);
}
- **`MathOperationImpl.java`(구현체)**
- 구현체는 실제 연산을 수행하는 클래스로, 인터페이스를 구현합니다.
- 이 클래스는 독립적으로 동작하며, 다른 곳에서는 이 구현체를 사용할 수 있습니다.
```java
// com/example/operation/MathOperationsImpl.java
package com.example.operations;
import com.example.interfaces.MathOperations;
public class MathOperationImpl implements MathOperations {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int subtract(int a, int b) {
return a - b;
}
}
Main.java(클라이언트 코드)
Main.java는 모듈화된 코드를 사용하는 부분으로, 인터페이스를 통해 MathOperations를 사용합니다.
이를 통해 MathOperationsImpl이 변경되더라도 클라이언트 코드는 영향을 받지 않습니다.
```java
// com/example/Main.java
package com.example;
import com.example.operations.MathOperationsImpl;
import com.example.interface.MathOperations;
public class Main {
public static void main(String[] args) {
// 인터페이스를 통해 의존성 관리
MathOperations mathOperations = new MathOperationsImpl();
int result = mathOperations.add(5, 3);
System.out.println(result); // 출력: 8
}
}
```
설명.
1. 인터페이스 분리
MathOperations 인터페이스를 정의하여, 이를 구현하는 클래스들이 독립적으로 개발될 수 있게 했습니다.
클라이언트 코드는 인터페이스만 알면 되고, 구체적인 구현체에 의존하지 않습니다.
2. 구현체 분리
MathOperationsImpl 클래스를 인터페이스로부터 분리하여 구현체를 독립적으로 관리할 수 있게 했습니다.
이렇게 하면 다른 구현체로 쉽게 교체할 수 있으며, 구현체의 변경이 클라이언트에 영향을 주지 않습니다.
3. 유연성 및 재사용성
인터페이스 기반의 설계는 유연성을 높이고, 다양한 환경에서 재사용 가능하게 만듭니다.
다른 프로젝트나 애플리케이션에서도 인터페이스만 있으면 쉽게 모듈을 교체하거나 사용할 수 있습니다.
4. 모듈 간 결합도 낮추기
모듈 간의 결합도를 낮추기 위해 인터페이스를 사용하여 모듈 간 통신을 추상화했습니다.
이제 Main 클래스는 MathOperation라는 추상화된 인터페이스를 사용하고, 구현체의 세부 사항을 알 필요가 없습니다.
4️⃣ 모듈화의 장점.
1. 유지보수성.
모듈화된 시스템은 각각의 모듈이 독립적으로 개발, 수정, 테스트될 수 있기 때문에 유지보수가 용이합니다.
변경이 필요한 부분만 수정하면 되므로 시스템 전체에 미치는 영향이 줄어듭니다.
2. 재사용성.
모듈화된 코드는 다양한 프로젝트에서 재사용할 수 있습니다.
동일한 모듈을 여러 시스템에서 공유함으로써 개발 시간과 비용을 절감할 수 있습니다.
3. 테스트 용이성.
모듈은 독립된 단위이므로 각 모듈을 개별적으로 테스트할 수 있습니다.
단위 테스트와 모듈 테스트가 용이해지며, 이를 통해 시스템의 품질을 높일 수 있습니다.
4. 개발 분업.
모듈화된 시스템에서는 팀이 각기 다른 모듈을 독립적으로 개발할 수 있습니다.
이는 프로젝트 관리와 협업에 유리하며, 더 빠른 개발 속도를 보장할 수 있습니다.
5️⃣ 결론
소프트웨어 공학에서의 모듈은 하나의 시스템을 구성하는 독립적이고 재사용 가능한 단위로, 시스템의 기능을 논리적으로 분리한 것입니다.
모듈화된 시스템은 복잡한 문제를 더 작은 문제로 나누어 해결하며, 재사용성, 유지보수성, 테스트 용이성 등 여러 가지 장점을 제공합니다.
모듈은 시스템을 구조화하고, 시스템 내에서 서로 독립적으로 동작하는 기능적 블록으로서 중요한 역할을 합니다.
-
💾 [CS] DIP의 정의에서 말하는 '추상화된 것'과 '추상화'의 개념의 차이점.
“💾 [CS] DIP의 정의에서 말하는 ‘추상화된 것’과 ‘추상화’의 개념의 차이점.”
DIP(Dependency Inversion Principle) 에서 말하는 “추상화된 것”과 소프트웨어 공학에서 사용하는 일반적인 “추상화” 개념은 서로 밀접하게 관련된 개념이지만, 문맥에 따라 강조점이 다를 수 있습니다.
1️⃣ DIP에서 말하는 “추상화된 것”
DIP에서 말하는 “추상화된 것” 은 구체적인 구현체가 아닌 인터페이스나 추상 클래스를 지칭합니다.
이 원칙에 따르면, 고수준 모듈(비즈니스 로직)은 저수준(세부적인 구현)에 의존해서 안 되며, 대신 두 모듈 모두 인터페이스나 추상 클래스 같은 추상화된 것에 의존해야 합니다.
이는 DIP에서 핵심적인 개념으로, 구현체 대신 계약(인터페이스)와 상호작용하도록 유도합니다.
🙋♂️ DIP의 정의에서 말하는 ‘추상화된 것’이란 무엇일까?
🙋♂️ 소프트웨어 공학에서의 모듈
🙋♂️ 모듈과 컴포넌트를 레고 블록에 비유해보면?!
🙋♂️ 비즈니스 로직(Business Logic)이란?
예시
// "추상화된 것"인 인터페이스
interface NotificationService {
void sendNotification(String message);
}
위의 NotificationService 인터페이스는 DIP의 문맥에서 말하는 “추상화된 것”입니다.
고수준 모듈과 저수준 모듈은 이 추상화된 인터페이스를 통해서만 서로 상호작용합니다.
2️⃣ 일반적인 “추상화” 개념.
소프트웨어 공학에서의 “추상화” 는 더 일반적인 개념으로, 세부사항을 숨기고 중요한 정보만을 드러내는 설계 기법을 의미합니다.
이를 통해 시스템의 복잡성을 줄이고, 설계나 구현에서 핵심적인 부분에 집중할 수 있게 해줍니다.
“추상화” 는 클래스, 인터페이스, 함수 등 다양한 수준에서 사용될 수 있으며, 주로 복잡한 시스템을 이해하기 쉽게 만들기 위해 사용됩니다.
“추상화” 는 구현 세부 사항을 감추고, 객체나 모듈 간의 상호작용을 간단하고 일관되게 만들어 줍니다.
예시.
abstract class Shape {
abstract void draw();
}
위의 Shape 클래스는 일반적인 추상화의 예로, 다양한 구체적인 도형(원, 사각형 등)의 세부 구현을 감추고 draw()라는 공통된 동작을 정의한 것입니다.
3️⃣ 차이점.
DIP에서의 “추상화된 것”
DIP에서 말하는 “추상화된 것”은 구현이 아닌 계약을 제공하는 인터페이스나 추상 클래스를 가리킵니다.
이 계약에 의존함으로써 고수준 모듈과 저수준 모듈의 결합도를 낮춥니다.
즉, 구현이 아닌, 추상적인 계약에 의존함으로써 유연성을 확보하고 의존성의 방향을 역전시킵니다.
일반적인 “추상화”
일반적인 “추상화” 는 더 광범위한 개념으로, 시스템 내에서 세부사항을 감추고 본질적인 개념만 드러내는 설계 기법입니다.
추상 클래스, 인터페이스뿐만 아니라, 복잡한 로직을 간단하게 만들기 위한 여러 기법이 포함됩니다.
4️⃣ 공통점
둘 다 세부 사항을 숨긴다.
DIP에서의 “추상화된 것”과 일반적인 “추상화” 모두 구체적인 구현 세부 사항을 감추고, 더 중요한 부분(주로 인터페이스나 상호작용)에 집중하도록 설계합니다.
유연성 제공
두 개념 모두 코드의 유연성을 증가시키고, 변경에 쉽게 대응할 수 있는 구조를 만드는데 기여합니다.
5️⃣ 결론
DIP에서의 “추상화된 것” 은 DIP 원칙을 적용할 때 구체적인 구현 대신 인터페이스나 추상 클래스와 같은 추상적 계층에 의존하도록 하는 구체적인 의미를 가집니다.
일반적인 “추상화” 는 소프트웨어 설계에서 복잡성을 관리하기 위해 사용하는 넓은 범위의 설계 개념으로, 객체 지향 프로그래밍의 기본 원칙 중 하나입니다.
결국, DIP에서 말하는 “추상화된 것”은 일반적인 “추상화”의 측정한 적용 사례라고 볼수 있습니다.
DIP는 추상화를 통해 시스템 간의 결합도를 낮추고 유연성을 높이는 것을 목표로 합니다.
-
💾 [CS] 소프트웨어 공학에서의 컴포넌트
💾 [CS] 소프트웨어 공학에서의 컴포넌트.
소프트웨어 공학에서의 컴포넌트는 재사용 가능하고 독립적인 소프트웨어 모듈을 의미합니다.
이 모듈은 명확하게 정의된 인터페이스를 통해 다른 컴포넌트나 시스템과 상호작용하며, 특정 기능이나 책임을 수행합니다.
컴포넌트는 소프트웨어 시스템의 더 큰 단위 또는 애플리케이션을 구성하는 기능적 단위로 볼수 있습니다.
1️⃣ 소프트웨어 공학에서의 컴포넌트의 주요 특성.
1. 독립성.
컴포넌트는 독립적으로 개발되고, 배포될 수 있습니다.
독립적으로 개발된 컴포넌트는 시스템의 다른 부분에 영향을 주지 않고 변경 또는 교체 될 수 있습니다.
2. 재사용성.
컴포넌트는 다양한 시스템에서 재사용될 수 있도록 설계됩니다.
한 번 개발된 컴포넌트는 다른 프로젝트나 애플리케이션에서 사용 가능하여 개발 생산성을 높이고, 중복된 개발을 줄입니다.
3. 명확한 인터페이스.
컴포넌트는 명확한 인터페이스를 가지고 있어야 하며, 이 인터페이스를 통해 다른 컴포넌트 또는 시스템과 상호작용합니다.
내부 구현은 감춰지고, 컴포넌트는 인터페이스를 통해서만 사용되므로 캡슐화가 이루어집니다.
4. 모듈성.
컴포넌트는 시스템을 구성하는 모듈 단위로서, 특정한 기능이나 작업을 수행합니다.
시스템의 복잡성을 줄이고, 이해하기 쉽게 하기 위해 컴포넌트는 모듈 단위로 분리되어 설계됩니다.
5. 느슨한 결합.
컴포넌트는 다른 컴포넌트와 느슨하게 결합되어 있어야 합니다.
이는 시스템의 유연성을 높이며, 각 컴포넌트가 독립적으로 개발되고 변경될 수 있게합니다.
6. 고응집성.
컴포넌트 내부적으로는 응집성이 높아야 합니다.
즉, 컴포넌트 내부의 요소들은 서로 밀접하게 관련된 작업을 수행해야 하며, 하나의 명확한 책임을 가져야 합니다.
2️⃣ 소프트웨어 컴포넌트 예시.
1. 사용자 인터페이스 컴포넌트.
버튼, 텍스트 필드, 드롭다운 메뉴와 같은 UI 요소는 컴포넌트로 간주될 수 있습니다.
이들은 각자 명확한 역할을 가지고 있으며, 다양한 애플리케이션에서 재사용될 수 있습니다.
2. 비즈니스 로직 컴포넌트.
예를 들어, 주문 처리 시스템에서 주문을 생성하고, 결제하고, 주문 상태를 업데이트하는 각각의 기능이 독립적인 컴포넌트로 분리될 수 있습니다.
각 컴포넌트는 특정 비즈니스 로직을 처리하며, 다른 시스템에서도 동일한 기능이 필요할 때 재사용될 수 있습니다.
3. 데이터 접근 컴포넌트(DAO).
데이터베이스와 상호작용하는 컴포넌트로, 데이터 CRUD(Create, Read, Update, Delete) 작업을 수행하는 모듈입니다.
DAO 컴포넌트는 데이터베이스의 구조나 기술이 변경되더라도, 인터페이스만 유지하면 다른 부분에 영향을 미치지 않도록 설계됩니다.
3️⃣ 컴포넌트 기반 개발(Component-Based Development, CBD)
컴포넌트 기반 개발(CBD) 은 소프트웨어 시스템을 컴포넌트 단위로 설계하고 개발하는 방법론입니다.
이 접근 방식은 시스템을 여러 개의 독립적이고 재사용 가능한 컴포넌트로 나누어 개발하며, 각 컴포넌트는 개별적으로 개발, 테스트, 배포될 수 있습니다.
1. 이점.
재사용성 : 한 번 개발된 컴포넌트는 여러 시스템에서 재사용 가능하여 개발 속도를 높이고, 유지보수를 용이하게 만듭니다.
유연성 : 컴포넌트 간 결합도가 낮기 때문에, 각 컴포넌트를 독립적으로 수정하거나 교체할 수 있어 시스템의 유연성이 높아집니다.
생산성 향상 : 재사용 가능한 컴포넌트를 통해 개발 속도를 높이고, 중복된 작업을 줄일 수 있습니다.
2. 예시.
전자 상거래 시스템에서 결제 처리 컴포넌트는 독립적으로 개발되어 신용카드 결제, 페이팔 결제 등 다양한 결제 방식을 지원할 수 있습니다.
이 컴포넌트는 다른 애플리케이션에서도 동일한 방식으로 재사용될 수 있습니다.
4️⃣ 결론.
소프트웨어 공학에서의 컴포넌트는 독립적이고 재사용 가능한 모듈로, 시스템의 모듈성, 유연성, 재사용성을 높이는 중요한 개념입니다.
컴포넌트는 명확하게 정의된 인터페이스를 통해 상호작용하며, 느슨하게 결합되어 변경에 대한 영향을 최소화할 수 있습니다.
이러한 컴포넌트 기반 개발 방식을 통해 시스템을 더 효율적으로 설계하고 개발할 수 있습니다.
-
💾 [CS] 모듈과 컴포넌트를 레고 블록에 비유해보면?!
💾 [CS] 모듈과 컴포넌트를 레고 블록에 비유해보면?!
모듈(Module) 과 컴포넌트(Component) 는 소프트웨어 공부를 하면 자주 보고, 듣는 용어 중 하나입니다.
이 두 용어의 개념을 자주 헷갈리거나 명확하게 알지 못해 이번에 공부를 해보았습니다.
제가 생각했을 때 재미있게도 소프트웨어는 레고 블록 같습니다.
그래서 이번에 모듈(Module) 과 컴포넌트(Component) 를 레고 블록에 비유하며 정리해봤습니다.
소프트웨어 공학에서의 모듈(Module) 과 컴포넌트(Component) 를 레고 블록에 비유하면, 이해하기 쉬운 방식으로 시스템 설계를 설명할 수 있습니다.
1️⃣ 레고 블록의 비유.
1. 모듈은 레고 세트.
모듈은 하나의 레고 세트와 비슷합니다.
레고 세트는 특정한 테마나 목표(예: 연필과 노트, 자동차, 집 등)를 가지고 있는 완성된 제품입니다.
여러 개의 레고 블록들이 모여서 더 큰 기능적 단위를 이루고, 이 세트가 완성되면 전체적인 형태나 목적을 나타냅니다.
마찬가지로, 소프트웨어 모듈은 소프트웨어 시스템 내에서 특정 기능이나 책임을 가진 완성된 구성 요소입니다.
모듈은 독립적으로 개발될 수 있으며, 다른 모듈과 함께 결합하여 더 큰 소프트웨어 시스템을 구성합니다.
비유.
예를 들어, 주문 처리 모듈은 레고 세트처럼 독립적으로 작동할 수 있지만, 레고 도시에 추가하면 그 도시의 전체 기능에 기여하게 됩니다.
모듈은 독립적이면서도 전체 시스템의 일부로 기능할 수 있는 구조입니다.
2. 컴포넌트는 레고 블록.
컴포넌트는 각각의 레고 블록에 비유할 수 있습니다.
레고 블록은 각자 독립적으로 존재할 수 있고, 단순한 모양에서부터 다양한 기능적 역할을 수행할 수 있는 모양까지 다양합니다.
이 블록들은 서로 결합하여 더 복잡한 구조를 만들고, 큰 레고 세트를 완성하는 데 사용됩니다.
비슷하게, 소프트웨어 컴포넌트는 각각의 독립적인 소프트웨어 단위로, 특정 기능을 수행합니다.
각 컴포넌트는 자신의 책임을 다하면서, 다른 컴포넌트와 상호작용하여 더 큰 모듈을 구성합니다.
비유.
예를 들어, 결제 컴포넌트, 배송 컴포넌트, 인증 컴포넌트 등은 각각의 레고 블록처럼 독립적으로 작동하며, 주문 처리 모듈이라는 레고 세트를 완성하는 데 기여할 수 있습니다.
2️⃣ 모듈과 컴포넌트의 상호작용을 레고에 비유해보자.
1. 모듈.
레고 세트는 여러 레고 블록(컴포넌트)으로 구성되어 있으며, 각각의 블록은 특정한 위치와 역할을 가집니다.
예를 들어, 자동차 레고 세트는 바퀴, 차체, 엔진 등의 블록으로 구성되며, 이 블록들이 결합되어 전체 자동차가 완성됩니다.
2. 컴포넌트.
레고 블록은 컴포넌트에 해당하며, 각각 독립적으로 존재하지만, 결합해서 더 복잡한 기능을 수행합니다.
차체 블록은 독립적으로는 차제일 뿐이지만, 바퀴와 결합되면 자동차로서 기능할 수 있습니다.
3️⃣ 결론적으로
컴포넌트는 레고 블록처럼 독립적으로 존재하며, 각각의 특정 기능을 수행하는 작은 단위입니다.
여러 블록을 결합하면 더 큰 구조를 형성할 수 있듯이, 여러 컴포넌트가 모여 더 복잡한 기능을 수행하는 소프트 웨어 시스템의 모듈을 형성합니다.
모듈은 레고 세트처럼 여러 블록(컴포넌트)을 결합해 완성된 기능적 단위로서 사용되며, 시스템 내에서 더 큰 역할을 담당하는 하나의 구성 요소가 됩니다.
3️⃣ 그렇다면 컴포넌트가 모듈보다 작은 단위라고 생각하면 되나요?
네, 맞습니다! 컴포넌트는 일반적으로 모듈보다 작은 단위로 생각할 수 있습니다.
소프트웨어 공학에서 컴포넌트와 모듈은 모두 독립적인 기능을 수행하는 단위이지만, 컴포넌트는 더 작은 재사용 가능한 기능 단위로, 여러 컴포넌트가 모여 하나의 모듈을 구성하는 경우가 많습니다.
1. 다시 레고 비유로 설명하자면~
컴포넌트는 개별적인 레고 블록과 같고, 각 블록은 독립적으로도 유용한 기능을 가지고 있지만, 여러 블록이 모여서 더 큰 구조(예: 연필과 노트, 자동차, 집 등)를 만듭니다.
모듈은 여러 레고 블록이 결합되어 완성된 레고 세트와 같다고 할 수 있습니다.
레고 세트는 연필과 노트, 자동차, 집 등과 같이 더 큰 단위의 구성 요소이며, 세트 내에서 여러 레고 블록(컴포넌트)이 함께 작동하여 더 복잡한 기능을 수행합니다.
2. 소프트웨어 공학에서의 관계.
1. 컴포넌트.
더 작은 기능적 단위입니다.
컴포넌트는 특정 작업이나 책임을 수행하는 코드의 묶음으로, 이를 독립적으로 재사용할 수 있습니다.
예를 들어, 결제 처리 기능, 배송 기능, 인증 기능 등이 컴포넌트가 될 수 있습니다.
2. 모듈.
여러 컴포넌트를 포함하는 더 큰 단위입니다.
모듈은 하나의 주요 기능이나 책임을 수행하는 소프트웨어의 큰 부분을 나타냅니다.
예를 들어, 주문 처리 모듈은 결제 컴포넌트, 배송 컴포넌트, 인증 컴포넌트 등의 여러 컴포넌트를 포함할 수 있습니다.
4️⃣ 마지막으로
컴포넌트는 보통 모듈을 구성하는 작은 단위이며, 특정한 작업이나 기능을 수행합니다.
모듈은 컴포넌트들을 묶어서 더 큰 기능을 제공하는 단위로, 더 복잡한 기능을 수행하는 소프트웨어 시스템의 일부를 형성합니다.
따라서, 컴포넌트가 모듈보다 작은 단위라고 생각하면 됩니다.
📝 참고 자료.
소프트웨어 공학에서의 모듈
소프트웨어 공학에서의 컴포넌트
-
🍃[Spring] 언제 `@Configuration`와 `@Bean`을 함께 사용할까?
🍃[Spring] 언제 @Configuration와 @Bean을 함께 사용할까?
@Configuration과 @Bean을 함께 사용하는 경우는 자바 기반의 설정 클래스 를 정의할 때입니다.
이 방식은 수동으로 빈을 정의하고 설정 과정을 세밀하게 제어하고자 할 때 사용됩니다.
특히 외부라이브러리나 프레임워크에서 제공하는 객체들을 빈으로 등록하거나, 빈의 생성과 초기화 과정을 커스터마이징해야 할 때 유용합니다.
1️⃣ @Configuration과 @Bean을 함께 사용하는 주요 이유.
1. 외부 라이브러리 클래스의 빈 등록.
@Configuration과 @Bean을 사용하면 외부 라이브러리의 클래스나, Spring이 직접 관리하지 않는 객체들을 빈으로 등록할 수 있습니다.
Spring의 자동 스캔 기능은 개발자가 작성한 클래스만 대상으로 하기 때문에, 외부 라이브러리에서 제공하는 객체는 수동으로 빈으로 등록해야 합니다.
2. 커스텀 빈 초기화 및 설정.
@Bean 어노테이션을 사용하면 빈이 생성되는 방식을 커스터마이징할 수 있습니다.
생성자 파라미터, 초기화 과정, 의존성 주입 방식 등을 세부적으로 설정할 수 있으며, 이 설정을 Java 코드로 작성함으로써 XML 기반 설정을 대체할 수 있습니다.
3. 복잡한 빈 생성 로직 처리.
빈 생성 과정이 복잡하거나 여러 의존성을 필요로 할 경우, @Configuration 클래스에서 이를 제어할 수 있습니다.
빈 간의 의존성, 특정 상황에 따른 빈 생성 로직 등을 자바 코드로 명확하게 관리할 수 있습니다.
4. XML 설정의 대체.
Spring은 과거에 XML 기반 설정을 주로 사용했지만, @Configuration과 @Bean을 사용하면 이러한 설정을 자바 코드로 관리할 수 있습니다.
자바 기반 설정은 타입 안정성을 제공하며, 코드와 설정이 하나의 파일 내에 통합되어 유지보수하기 쉬워집니다.
2️⃣ 예시
1. 외부 라이브러리 빈 등록
예를 들어, 외부 라이브러리의 데이터베이스 커넥션 풀(HikariDataSource)을 빈으로 등록하고, 이를 커스터마이징하는 경우입니다.
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("password");
dataSource.setMaximumPoolSize(10);
return dataSource;
}
}
@Configuration
이 클래스가 Spring의 설정 파일로 사용되며, 빈을 정의하는 클래스임을 나타냅니다.
@Bean
이 메서드가 반환하는 HikariDataSource 객체는 스프링 컨테이너에 빈으로 등록됩니다.
외부 라이브러리 객체이므로 자동 스캔이 불가능하며, 이를 수동으로 등록하는 방식입니다.
2. 복잡한 빈 초기화
다음은 서비스 빈이 여러 가지 의존성을 필요로 하고, 빈 초기화 과정에서 특정 설정이 필요한 경우입니다.
@Configuration
public class AppConfig {
@Bean
public UserService userService(UserRepository userRepository, NotificationService notificationService) {
UserService userService = new UserService(userRepository);
userService.setNotificationService(notificationService); // 추가 설정
return userService;
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
@Bean
public NotificationService notificationService() {
return new EmailNotificationService();
}
}
이 예에서는 UserService가 여러 의존성을 필요로 하고, 특정 추가 설정이 필요한 상황을 처리합니다.
Configuration : 애플리케이션의 설정 클래스임을 명시합니다.
@Bean : UserService, UserRepository, NotificationService를 빈으로 등록하고, 의존성을 수동으로 주입하고 추가 설정을 할 수 있습니다.
3. 빈 간 의존성 관리.
@Configuration 클래스에서 빈 간의 의존성을 명확하게 관리할 수 있습니다.
다음은 여러 빈 간의 의존성을 처리하는 예시입니다.
@Configuration
public class ServiceConfig {
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
}
여기서 UserService는 UserRepository에 의존하며, @Bean 메서드를 통해 UserRepository 빈을 참조합니다.
스프링은 이러한 의존성을 자동으로 해결하여 빈을 등록합니다.
3️⃣ 언제 @Configuration과 @Bean을 사용해야 할까?
1. 외부 라이브러리 또는 스프링이 자동으로 관리하지 않는 클래스 등록
외부 라이브러리의 클래스나 다른 프레임워크에서 제공하는 객체들을 빈으로 등록해야 할 때 @Configuration과 @Bean을 함께 사용합니다.
2. 복잡한 빈 생성 로직이 필요할 때
빈 생성 시 의존성 주입 외에 추가적인 설정이 필요한 경우(특정 메서드 호출, 객체 초기화, 추가 설정 등) @Bean을 사용하여 수동으로 빈을 생성하고, 이를 커스터마이징할 수 있습니다.
3. 빈 간의 명시적 의존성 관리
서로 의존하는 빈들이 있을 때, @Configuration 클래스에서 빈의 생성 순서와 의존성을 명시적으로 관리할 수 있습니다.
4. 유연한 설정이 필요할 때
애플리케이션 설정을 자바 코드로 관리하면서, 조건부 빈 생성, 프로파일 기반 빈 관리 등과 같이 더 복잡하고 유연한 설정이 필요할 때 유용합니다.
4️⃣ 결론
@Configuration과 @Bean은 스프링 애플리케이션에서 빈을 수동으로 등록하고 설정하는 방식으로 사용됩니다.
이 두 어노테이션을 함께 사용함으로써 스프링은 자바 기반으로 빈을 생성하고 관리할 수 있게 되며, 외부 라이브러리나 스프링이 자동으로 관리하지 않는 객체들도 빈으로 등록할 수 있습니다.
이러한 방식은 특히 더 복잡한 빈 생성 로직을 필요로 하거나 외부 리소스와의 통합이 필요할 때 유용합니다.
-
🍃[Spring] 언제 `@Service`,`@Repository`,`@Controller`와 같은 어노테이션을 사용할까?
🍃[Spring] 언제 @Service,@Repository,@Controller와 같은 어노테이션을 사용할까?
@Service,@Repository,@Controller와 같은 어노테이션은 Spring Framework에서 특정 레이어의 역할을 명확히 하고, 자동으로 빈을 등록할 때 사용됩니다.
이 어노테이션들은 @Component 어노테이션의 특수화된 버전으로, 각각의 레이어를 구분하여 Spring 애플리케이션을 더 구조화하고, 책임을 명확하게 하기 위해 사용됩니다.
1️⃣ @Service
사용 시점
비즈니스 로직을 처리하는 서비스 계층에서 사용됩니다.
설명
@Service는 애플리케이션 내에서 핵심 비즈니스 로직을 구현하는 클래스에 붙입니다.
이 계층은 컨트롤러에서 전달된 요청을 처리하고, 데이터를 조작하거나 다른 비즈니스 규칙을 적용합니다.
또한, 이 계층은 트랜잭션 관리나 예외 처리와 같은 중요한 작업도 수행할 수 있습니다.
사용 예시
@Service
public class UserService {
public User findUserById(Long id) {
// 비즈니스 로직 처리
return userRepository.findById(id).orElseThrow();
}
}
사용 목적
@Service를 사용함으로써 해당 클래스가 비즈니스 로직을 담당하는 서비스 계층의 역할을 한다는 점을 명확히 하고, Spring 컨테이너에 의해 자동으로 빈으로 등록되게 합니다.
또한, @Service 어노테이션을 통해 Spring이 해당 클래스에 대해 추가적인 처리를 적용할 수 있습니다(예: 트랜잭션 관리).
2️⃣ @Repository
사용 시점
데이터 접근 계층(DAO, Data Access Object) 에서 사용됩니다.
설명
@Repository는 데이터베이스와 상호작용하는 영속성 계층에서 사용됩니다.
보통 데이터베이스 CRUD 작업을 수행하며, 데이터베이스와의 직접적인 연결, 쿼리 실행, 결과 처리 등을 담당합니다.
Spring에서는 @Repository를 사용하여 데이터베이스 예외를 Spring 예외로 변환하는 등의 추가적인 기능도 제공합니다.
사용 예시
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 데이터베이스 작업을 위한 메서드 정의
}
사용 목적
@Repository는 해당 클래스가 데이터베이스와 상호작용하는 DAO 역할을 한다는 점을 명확히하고, 자동으로 빈으로 등록되게 합니다.
또한, @Repository는 데이터베이스와 관련된 예외를 표준화된 Spring 예외로 변환하는 기능을 제공합니다.
3️⃣ @Controller
사용 시점
웹 계층(프레젠테이션 계층) 에서 사용됩니다.
설명
@Controller는 사용자 요청을 처리하고, 적절한 응답을 반환하는 역할을 하는 웹 컨트롤러 클래스에 사용됩니다.
주로, Spring MVC 애플리케이션에서 사용되며, 클라이언트 요청을 받아 비즈니스 로직을 처리하고, 결과를 HTML 페이지나 JSON 형식으로 반환합니다.
사용 예시
@Controller
public class UserController {
@GetMapping("/user/{id}")
public String getUser(@PathVariable Long id, Model model) {
User user = userServie.findUserById(id);
model.addAttribute("user", user);
return "userDetail";
}
}
사용 목적
@Controller는 해당 클래스가 웹 요청을 처리하는 컨트롤러임을 명확히 하며, Spring 컨테이너에 의해 자동으로 빈으로 등록됩니다.
웹 요청을 받아서 처리하고, 응답을 생성하는 역할을 하기 때문에 사용자와 애플리케이션 간의 인테페이스 역할을 합니다.
4️⃣ @RestController
사용 시점
RESTful 웹 서비스 계층에서 사용됩니다.
설명
@RestController는 @Controller와 @ResponseBody가 결합된 어노테이션으로, 주로 JSON 또는 XML 형식의 데이터를 반환하는 RESTful API를 만들 때 사용됩니다.
Spring MVC에서 데이터를 직렬화하여 클라이언트에게 전송할 때 사용됩니다.
사용 예시
@RestController
public class UserRestController {
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findUserById(id);
}
}
사용 목적
@RestController는 주로 REST API 를 개발할 때 사용되며, 컨트롤러에서 반환하는 데이터를 HTML 페이지가 아닌 JSON이나 XML과 같은 형식으로 반환합니다.
REST API 설계를 할 때 이 어노테이션을 사용하면 개발자의 의도를 명확히 전달할 수 있습니다.
5️⃣ 언제 사용해야 하는가?
@Service
비즈니스 로직을 담당하는 클래스에 사용합니다.
데이터 접근 계층에서 가져온 데이터를 가공하거나 규칙을 적용하는 등의 작업을 수행하는 곳입니다.
@Repository
데이터베이스와의 상호작용을 처리하는 클래스에 사용합니다.
주로 데이터 저장, 수정, 조회, 삭제와 같은 영속성 로직이 포함된 DAO 또는 리포지토리에 붙입니다.
@Controller
사용자로부터 HTTP 요청을 받아 응답을 생성하는 웹 컨트롤러에 사용합니다.
주로 Spring MVC에서 동적인 웹 페이즈를 랜더링할 때 사용됩니다.
@RestController
RESTful 웹 서비스를 제공하는 컨트롤러에 사용합니다.
이 어노테이션을 사용하면 웹 요청을 처리한 후 JSON 또는 XML 형식의 데이터를 반환할 수 있습니다.
6️⃣ 결론
이 어노테이션들은 각각의 클래스가 어떤 역할을 담당하는지 명확히 구분해 줌으로써, Spring 애플리케이션의 구조화와 관리에 도움을 줍니다.
Spring 컨테이너는 이 어노테이션이 붙은 클래스들을 자동으로 감지하여 빈으로 등록하고, 필요한 곳에 의존성을 주입해줍니다.
-
🍃[Spring] Spring에서 빈(Bean)을 주입받는 방법들.
🍃[Spring] Spring에서 빈(Bean)을 주입받는 방법들.
Spring에서 빈(Bean) 을 주입받는 방법에는 여러 가지가 있으며, 주로 의존성 주입(Dependency Injection, DI) 이라는 개념을 통해 이루어집니다.
Spring IoC 컨테이너는 객체 간의 의존성을 관리하고, 필요한 곳에 자동으로 빈을 주입합니다.
빈을 주입받는 방법에는 생성자 주입, 세터 주입, 필드 주입이 있습니다.
🙋♂️ 의존성(Dependency)
🙋♂️ Spring 컨테이너를 사용하는 이유
🙋♂️ Spring 컨테이너
🙋♂️ Spring 빈(Bean)
1️⃣ 생성자 주입(Constructor Injection)
생성자 주입은 의존성을 주입할 때 생성자를 통해 빈을 주입하는 방식입니다.
가장 권장되는 방식 중 하나로, 의존성을 강제하고 불변성을 보장할 수 있습니다.
또한, 테스트하기 용이한 방식입니다.
예시
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired // Spring 4.3+ 에서는 생략 가능
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void process() {
userRepository.save();
}
}
설명.
UserService는 UserRepository 빈을 생성자를 통해 주입받습니다.
@Autowired를 통해 스프링이 UserRepository 빈을 자동으로 주입하게 됩니다.
@Autowired는 Spring 4.3 이후 생성자 주입에서는 생략 가능하지만, 명시적으로 적는 경우도 있습니다.
2️⃣ 세터 주입(Setter Injection)
세터 주입은 세터 메서드를 통해 빈을 주입하는 방식입니다.
선택적인 의존성을 주입할 때 유용하며, 주입받은 빈을 변경할 수 있는 유연성을 제공합니다.
예시
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRespository = userRepository;
}
public void process() {
userRepository.sava();
}
}
설명.
UserSevice는 setUserRepository라는 세터 메서드를 통해 UserRepository 빈을 주입받습니다.
@Autowired 어노테이션을 통해 스프링이 적절한 빈을 주입하게 됩니다.
3️⃣ 필드 주입(Field Injection)
필드 주입은 직접 필드에 @Autowired 어노테이션을 붙여서 빈을 주입하는 방식입니다.
가장 간단한 방식이지만, 테스트하기 어려운 구조를 만들 수 있고, 주입된 필드가 ‘final’로 설정되지 않기 때문에 ‘불변성’이 보장되지 않습니다.
일반적으로는 지양하는 방식입니다.
예시
@Servicee
public class UserService {
@Autowired
private UserRepository userRepository;
public void process() {
userRepository.save();
}
}
설명.
UserService는 UserRepository 빈을 필드에 직접 주입받습니다.
필드 주입 방식은 코드가 간결하지만, 테스트나 유지보수 측면에서 불리할 수 있습니다.
4️⃣ 각 주입 방식의 비교.
1. 생성자 주입(Constructor Injection)
장점
의존성이 필수적임을 강제할 수 있고, 불변성을 보장하며, 테스트하기 용이합니다.
의존성이 주입되지 않으면 컴파일 타임에 오류를 발견할 수 있습니다.
단점
클래스가 많은 의존성을 가질 경우, 생성 인자가 많아질 수 있습니다.
2. 세터 주입(Setter Injection)
장점
선택적인 의존성 주입이 가능하며, 객체 생성 후에 주입할 수 있어 유연성을 제공합니다.
단점
의존성이 주입되지 않은 상태로 사용될 위험이 존재하며, 객체의 상태가 변경될 수 있습니다.
3. 필드 주입(Field Injection)
장점
코드가 간결하고 가장 쉽습니다.
단점
테스트하기 어렵고, 의존성을 강제하지 않으며, 리플렉션을 사용하기 때문에 불변성이 보장되지 않습니다.
또한, 필드에 접근하는 방식이기 때문에 SRP(Single Responsibility Principle)를 위반할 가능성이 높습니다.
🙋♂️ SOLID 원칙
5️⃣ 결론
생성자 주입(Constructor Injection) : 생성자 주입(Constructor Injection) 은 의존성 강제, 불변성 보장, 테스트 용이성 측면에서 가장 권장되는 방식입니다.
세터 주입(Setter Injection) : 선택적 의존성을 주입할 때 유용하지만, 세터 메서드가 공용으로 노출된다는 단점이 있습니다.
필드 주입(Field Injection) : 가장 간단한 방식이지만, 테스트가 어렵고 불변성을 보장하지 않기 때문에 지양하는 방식입니다.
-
🍃[Spring] 빈(Bean)을 등록하는 방법.
🍃[Spring] 빈(Bean)을 등록하는 방법.
1️⃣ @Configuration 어노테이션.
@Configuration 어노테이션은 Spring Framework에서 자바 기반의 설정 클래스를 정의할 때 사용하는 어노테이션입니다.
이 어노테이션이 붙은 클래스는 스프링 컨테이너에 의해 빈 정의를 제공하는 클래스로 인식되며, 일반적으로 메서드를 통해 빈을 생성하고, 이를 스프링 컨테이너에 등록하는 역할을 합니다.
1. @Configuration의 주요 기능.
1. 자바 기반 설정 클래스.
@Configuration 어노테이션은 자바 코드로 스프링 설정을 관리할 수 있도록 해줍니다.
전통적인 XML 기반 설정 대신, 자바 클래스를 사용해 애플리케이션의 설정을 관리하고, 빈을 정의할 수 있습니다.
2. 빈 정의.
@Configuration 어노테이션이 붙은 클래스 내에서 정의된 메서드에 @Bean 어노테이션을 사용하면, 해당 메서드가 반환하는 객체가 스프링 컨테이너에 빈으로 등록됩니다.
이 방식으로 객체의 생성과 초기화 과정을 설정하고 관리할 수 있습니다.
3. 싱글톤 보장.
@Configuration이 붙은 클래스는 기본적으로 싱글톤 빈을 보장합니다.
즉, 이 클래스 내에서 정의된 빈은 스프링 컨테이너 내에서 한 번만 생성되고, 다른 곳에서 해당 빈을 요청할 때 동일한 인스턴스가 반환됩니다.
이는 클래스가 @Configuration이 아닌 일반 클래스일 경우와 차별화되는 중요한 특성입니다.
2. 예시.
다음은 @Configuration 어노테이션을 사용하여 자바 기반으로 빈을 등록하는 예시입니다.
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
}
3. 설명.
@Confifuration
AppConfig 클래스가 스프링 컨테이너에서 설정 클래스임을 나타냅니다.
이 클래스는 스프링 애플리케이션의 빈을 정의하는 역할을 합니다.
@Bean
이 어노테이션이 붙은 메서드는 스프링 컨테이너에 빈을 등록합니다.
userService와 userRepository 메서드는 각각 UserService와 UserRepository 객체를 반환하며, 이 객체들은 스프링 컨테이너에 의해 빈으로 관리됩니다.
4. @Configuration의 특징.
1. 싱글톤 관리.
@Configuration 어노테이션이 붙은 클래스 내에서 정의된 빈들은 기본적으로 싱글톤으로 관리됩니다.
즉, 여러 번 요청하더라도 동일한 인스턴스가 반환됩니다.
스프링이 내부적으로 프록시 클래스를 사용하여 빈의 싱글톤 속성을 보장합니다.
2. 모듈화된 설정.
@Configuration을 사용하면 애플리케이션의 설정을 여러 자바 클래스로 나누어 모듈화할 수 있습니다.
이를 통해 설정 파일이 커지더라도 관리하기 쉽고, 유지보수성이 향상됩니다.
3. 다른 설정 파일과 통합.
@Configuration 클래스는 다른 설정 파일이나 XML 파일과 함꼐 사용될 수 있습니다.
이로 인해 기본 XML 기반 설정을 점진적으로 자바 기반 설정으로 변환하는 것이 가능합니다.
5. @Configuration과 @Component의 차이.
@Configuration과 @Component는 모두 스프링 컨테이너에 빈을 등록할 수 있는 어노테이션이지만, 그 목적과 기능에는 차이가 있습니다.
@Configuration
주로 설정 클래스에 사용됩니다.
이 클래스는 @Bean 어노테이션을 사용해 빈을 정의하고, 컨테이너에 등록할 여러 빈을 한곳에서 관리합니다.
이 빈들은 주로 싱글톤으로 관리되며, 구성 요소들의 관계를 설정하는 데 사용됩니다.
@Component
일반적으로 단일 빈을 자동으로 등록할 때 사용됩니다.
클래스에 @Component를 붙이면 스프링이 자동으로 해당 클래스를 스캔하여 빈으로 등록합니다.
보통 특정 역할을 하는 개별 클래스를 빈으로 등록할 때 사용됩니다.
6. 결론.
@Configuration 어노테이션은 자바 기반의 설정 클래스를 정의하는 데 사용되며, 스프링 컨테이너에서 관리될 빈을 등록하는 중요한 역할을 합니다.
이를 통해 애플리케이션의 설정을 더욱 명확하고 모듈화된 방식으로 관리할 수 있으며, XML 기반 설정을 대체하거나 보완하는 용도로 사용됩니다.
Configuration 클래스 내의 메서드는 빈을 정의하고 이를 컨테이너에 등록하여, 스프링 애플리케이션의 동작을 제어하는 중요한 기능을 수행합니다.
2️⃣ @Bean 어노테이션
@Bean 어노테이션은 Spring Framework에서 메서드 수준에서 사용되며, 해당 메서드가 반환하는 객체를 스프링 컨테이너에 빈(Bean)으로 등록하기 위해 사용됩니다.
이 어노테이션은 주로 자바 기반의 설정 클래스(@Configuration)에서 사용되며, 빈의 생성 및 초기화를 담당하는 역할을 합니다.
1. @Bean 어노테이션의 주요 기능.
1. 스프링 컨테이너에 빈 등록.
@Bean 어노테이션이 붙은 메서드가 반환하는 객체는 스프링 컨테이너에 의해 관리되는 빈으로 등록됩니다.
이 빈은 스프링 애플리케이션에서 의존성 주입(Depency Injection, DI)을 통해 다른 클래스에서 사용할 수 있습니다.
2. 메서드 호출 시 빈 반환.
@Bean 메서드는 스프링 컨테이너에서 호출되어 해당 메서드가 반환하는 객체를 관리합니다.
이 빈은 컨테이너에서 여러 번 요청되더라도 기본적으로 싱글톤으로 관리됩니다.(즉, 동일한 인스턴스가 반환됨).
3. 자바 기반 설정 지원.
@Bean 어노테이션은 자바 기반의 설정 클래스(@Configuration)에서 사용되어, XML 설정을 대체 할 수 있습니다.
이를 통해 객체 간의 관계나 초기화 과정을 프로그래밍 방식으로 정의할 수 있습니다.
2. 예시 코드
다음은 @Bean 어노테이션을 사용하는 간단한 예입니다.
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
}
3. 설명.
@Configuration
이 클래스가 스프링 설정 클래스로 사용됨을 나타냅니다.
이 클래스 안에 빈을 정의하는 메서드가 포함됩니다.
@Bean
각 메서드가 반환하는 객체를 스프링 컨테이너에 빈으로 등록합니다.
예를 들어, userService() 메서드는 userService 객체를 반환하며, 이 객체는 스프링 컨테이너에 의해 빈으로 관리됩니다.
4. 빈의 생명주기와 관리.
싱글톤 관리
기본적으로 스프링 컨테이너는 @Bean으로 등록된 빈을 싱글톤으로 관리합니다.
즉, 여러 곳에서 같은 빈을 요청하더라도 동일한 인스턴스가 반환됩니다.
스코프 변경 가능
@Bean 어노테이션을 사용할 때 스코프를 지정할 수 있습니다.
예를 들어, 빈이 요청될 때마다 새로운 객체를 반환하는 프로토타입 스코프로 변경할 수 있습니다.
이는 @Scope 어노테이션을 함께 사용하여 설정합니다.
5. @Bean 과 @Component의 차이
@Bean
@Bean은 메서드 수준에서 사용되며, 해당 메서드가 반환하는 객체를 빈으로 등록합니다.
주로 사바 설정 클래스에서 사용되어 수동으로 빈을 정의하는 방식입니다.
이를 통해 개발자가 객체 생성 로직을 명시적으로 작성할 수 있습니다.
@Component
@Component는 클래스 수준에서 사용되며, 스프링이 해당 클래스를 스캔하여 자동으로 빈으로 등록하게 합니다.
개발자가 별도의 메서드 없이 클래스 자체를 빈으로 등록하는 자동 빈 등록 방식입니다.
@Service, @Repository, @Controller도 @Component의 특수화된 형태입니다.
6. 추가 기능
@Bean 파라미터
이름 지정
@Bean 어노테이션은 이름을 명시적으로 지정할 수 있습니다. 기본적으로 메서드 이름이 빈 이름이 되지만, @Bean(name = "customName")과 같이 빈 이름을 명시할 수 있습니다.
@Bean(name = "custromUserService")
public UserService userService() {
return new UserService(userRepository());
}
의존성 관리
@Bean 메서드는 다른 빈을 의존성으로 사용할 수 있습니다.
위의 예시에서 userService() 메서드는 userRepository() 메서드에서 반환된 UserRepository 빈을 사용합니다.
이런한 방식으로 빈 간의 의존성을 설정할 수 있습니다.
7. 결론
@Bean 어노테이션은 스프링에서 자바 기반으로 빈을 정의하고, 스프링 컨테이너에 등록할 때 사용됩니다.
주로 설정 클래스(@Configuration) 내에서 사용되며, 프로그래밍 방식으로 객체의 생성과 설정을 관리하는 역할을 합니다.
이 어노테이션은 수동으로 빈을 정의할 때 사용되며, XML 설정을 대체하거나 보완하는 방식으로 사용됩니다.
@Component와는 달리 빈 생성 로직을 더 명확하게 제어할 수 있는 점이 특징입니다.
-
🍃[Spring] `@Qualifier` 어노테이션.
🍃[Spring] @Qualifier 어노테이션.
@Qualifier 어노테이션은 Spring Framework에서 빈 주입 시 모호성을 해결하기 위해 사용되는 어노테이션입니다.
Spring은 기본적으로 타입을 기준으로 빈을 주입하지만, 동일한 타입의 빈이 여러 개 존재할 경우 어느 빈을 주입할지 모호성이 발생할 수 있습니다.
이때 @Qualifier 어노테이션을 사용하여 특정 빈을 명시적으로 지정할 수 있습니다.
1️⃣ @Qualifier의 주요 기능.
1. 명시적 빈 선택.
여러 개의 동일한 타입의 빈이 존재할 때, @Qualifier를 사용하여 어떤 빈을 주입할지 명시적으로 지정할 수 있습니다.
이를 통해 Spring이 주입해야 할 빈을 명확하게 구분할 수 있습니다.
2. 빈 이름 기반 주입.
@Qualifier는 빈의 이름을 기준으로 주입할 빈을 선택합니다.
@Autowired와 함께 사용되며, 이를 통해 Spring이 어떤 빈을 주입할지 결정할 수 있습니다.
2️⃣ 사용 예시
1. 동일한 타입의 여러 빈이 있을 때.
@Component
public class FirstService implements MyService {
// FirstService 구현
}
@Component
public class SecondService implements MyService {
// SecondService 구현
}
위 코드에서 FirstService와 SecondService가 모두 MyService 타입으로 정의된 빈입니다.
이 경우 MyService 타입의 빈을 주입받으려 하면 Spring이 어느 빈을 주입해야 할지 모호성이 발생합니다.
2. @Qualifier로 특정 빈 주입하기.
@Service
public class MyClient {
private final MyService myService;
@Autowired
public MyClient(@Qualifier("secondService") MyService myService) {
this.myService = myService;
}
public void execute() {
myService.performAction();
}
}
설명
위 코드에서 @Qualifier("secondService")는 SecondService 빈을 명시적으로 주입하도록 지정하고 있습니다.
따라서 Spring은 SecondService 빈을 주입하게 됩니다.
동일한 타입의 여러 빈이 있을 때, 이와 같이 명시적으로 선택할 수 있습니다.
3️⃣ 사용 상황.
동일한 타입의 빈이 여러 개 있을 때
Qualifier는 여러 개의 동일한 타입의 빈이 등록되어 있을 때, 어느 빈을 주입해야 할지 명확히 지정해야 하는 경우에 사용됩니다.
특정 빈을 주입하고 싶을 때
일반적인 상황에서 기본 빈이 아닌, 특정한 빈을 주입하고자 할 때 사용할 수 있습니다.
빈 이름 지정.
빈 이름을 명시적으로 지정하려면, @Component 또는 @Bean 어노테이션에 이름을 지정할 수 있습니다.
```java
@Component(“firstService”)
public class FirstService implements MyService {
// 구현 내용
}
@Component(“secondService”)
public class SecondService implements MyService {
// 구현 내용
}
- 이렇게 빈의 이름을 명시적으로 설정한 후 `@Qualifier`로 해당 이름을 지정하여 주입할 수 있습니다.
## 4️⃣ `@Qualifier`와 `@Primary`의 차이
- **`@Primary`**
- 기본적으로 사용될 빈을 지정합니다.
- 동일한 타입의 여러 빈이 존재할 때, `@Primary`가 지정된 빈이 우선적으로 주입됩니다.
- 다만, 명시적으로 `@Qualifier`가 사용되면 `@Primary`는 무시됩니다.
- **`@Qualifier`**
- 특정 빈을 명시적으로 주입할 때 사용됩니다.
- `@Primary`가 설정된 빈이 있더라도, `@Qualifier`로 명시된 빈이 우선됩니다.
## 5️⃣ 예시: `@Primary`와 `@Qualifier` 함께 사용.
```java
@Component
@Primary
public class FirstService implements MyService {
// FirstService 구현
}
@Component
public class SecondService implements MyService {
// SecondService 구현
}
@Service
public class MyClient {
private final MyService myService;
@Autowired
public MyClient(@Qualifier("secondService") MyService myService) {
this.myService = myService;
}
public void execute() {
myService.performAction();
}
}
설명
여기서 FirstService는 @Primary로 기본 빈으로 설정되었지만, @Qualifier("secondService")를 사용해 SecondService 빈이 명시적으로 주입됩니다.
@Primary는 기본 빈을 설정할 때 유용하고, @Qualifier는 특정 상황에서 특정 빈을 주입할 때 사용됩니다.
6️⃣ 결론
@Qualifier 어노테이션은 Spring에서 동일한 타입의 여러 빈 중 특정 빈을 명시적으로 주입해야 할 때 사용됩니다.
이를 통해 빈 주입 과정에서 발생할 수 있는 모호성을 해결할 수 있으며, 개발자가 원하는 빈을 명확히 지정할 수 있습니다.
@Primary와 함께 사용하여 기본 빈과 특정 빈을 관리할 수 있습니다.
-
🍃[Spring] `@Component` 어노테이션.
🍃[Spring] @Component 어노테이션.
@Component 어노테이션은 Spring Framework에서 빈(Bean) 으로 등록할 클래스를 지정하기 위해 사용하는 클래스 레벨 어노테이션입니다.
이 어노테이션을 사용하면 해당 클래스가 Spring IoC 컨테이너에 의해 자동으로 관리되는 빈으로 등록됩니다.
주로 애플리케이션에서 자동으로 빈을 등록하고 싶을 때 사용 됩니다.
🙋♂️ Spring 컨테이너
🙋♂️ Spring 컨테이너를 사용하는 이유
📝 Spring IoC 컨테이너와 Spring 컨테이너는 다른 개념인가요?
Spring IoC 컨테이너와 Spring 컨테이너는 같은 개념을 의미하는 용어입니다.
이 두 용어는 모두 Spring Framework에서 객체(Bean, 빈)의 생성, 관리, 의존성 주입, 생명주기 관리 등을 담당하는 컨테이너를 가리킵니다.
📝 같은 개념에 대한 다양한 표현
Spring IoC 컨테이너는 더 구체적인 용어로, Spring에서 Inversion Of Control(제어의 역적) 원칙을 구현하는 빈 관리 시스템을 가리킵니다.
이 용어는 주로 제어의 역전(Inversion of Control) 이라는 프로그래밍 원칙을 강조하기 위해 사용됩니다.
IoC는 개발자가 직접 객체를 생성하고 관리하지 않고, 컨테이너가 객체의 생성과 의존성을 관리하는 방식입니다.
Spring 컨테이너는 더 일반적인 용어로, Spring Framework가 제공하는 객체 관리 시스템을 가리키는 표현입니다.
이 용어는 Spring이 제공하는 컨테이너의 기능을 포괄적으로 표현하며, 그 핵심 기능은 IoC 컨테이너 입니다.
📝 결론
Spring IoC 컨테이너와 Spring 컨테이너는 같은 개념으로 볼 수 있으며, 두 용어 모두 Spring의 핵심 기능인 객체 관리와 의존성 주입을 담당하는 시스템을 가리킵니다.
IoC는 이 컨테이너의 작동 원리를 설명하는 용어이고, Spring 컨테이너는 이를 일반적으로 부를 때 사용하는 표현힙니다.
1️⃣ 주요 기능 및 특징.
1. 자동 빈 등록.
@Component 어노테이션이 붙은 클래스는 Spring의 컴포넌트 스캔 가능에 의해 자동으로 감지되고, Spring IoC 컨테이너에 빈으로 등록됩니다.
개발자가 직접 빈을 등록하는 대신, Spring이 클래스 경로에서 이를 탐색하고 관리하게 됩니다.
2. 다른 특화된 어노테이션의 기반
@Component는 Spring에서 일반적인 빈을 등록하는 용도로 사용되며, 이를 기반으로 한 더 구체적인 어노테이션이 있습니다.
예를 들어, @Service, @Repository, @Controller 등이 @Component의 특수화된 형태로, 각각의 역할에 맞는 빈을 구체적으로 정의합니다.
@Service: 서비스 레이어를 정의하는 클래스에 사용.
@Repository: 데이터 엑세스 레이어(DAO)에 사용.
@Controller: 웹 컨트롤러(프레젠테이션 레이어)에 사용.
3. 간편한 빈 관리
@Component는 특별한 설정 없이 클래스에 간단히 어노테이션만 붙여서 Spring IoC 컨테이너에서 관리되는 빈으로 만들 수 있습니다.
Spring이 제공하는 기본적인 빈 등록 방식 중 하나입니다.
2️⃣ 예시
@Component
public class MyComponent {
public void doSomething() {
System.out.println("Hello from MyComponent!");
}
}
위 코드에서 MyComponent 클래스는 @Component 어노테이션 덕분에 Spring IoC 컨테이너에 자동으로 빈으로 등록됩니다.
이제 Spring이 이 빈을 관리하며, 다른 곳에서 의존성 주입을 통해 사용할 수 있습니다.
@Service
public class MyService {
private final MyComponent myComponent;
@Autowired
public MyService(MyComponent myComponent) {
this.myComponent = myComponent;
}
public void performAction() {
myComponent.doSomthing();
}
}
이 예에서는 MyService 클래스가 MyComponent 빈을 주입받아 사용합니다.
MyComponent 가 자동으로 빈으로 등록되었기 때문에, @Autowired를 통해 MyService에 주입될 수 있습니다.
3️⃣ @Component와 컴포넌트 스캔
@Component 어노테이션이 사용되려면 Spring이 컴포넌트 스캔을 통해 해당 클래스를 찾아야 합니다.
일반적으로 @ComponentScan 어노테이션이나 Spring Boot에서는 @SpringBootApplication 어노테이션을 통해 자동으로 컴포넌트 스캔이 활성화됩니다.
@SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
이 코드에서 @SpringBootApplication은 @ComponentScan을 포함하고 있어, @Component가 붙은 모든 클래스를 자동으로 검색하고 빈으로 등록합니다.
4️⃣ @Component와 다른 빈 등록 방식 비교.
@Component vs @Bean
@Component는 클래스 레벨에서 자동으로 빈을 등록하며, 컴포넌트 스캔을 통해 Spring이 클래스를 감지합니다.
@Bean은 메서드 레벨에서 수동으로 빈을 등록하는 방식으로, 자바 설정 클래스(@Configuration)내에서 사용됩니다.
개발자가 직접 메서드를 통해 빈을 생성하고 초기화할 수 있습니다.
@Component vs @Service, @Repository, @Controller
이들 어노테이션은 모두 @Component를 기반으로 하며, 특정 레이어의 역할을 나타내기 위해 사용됩니다.
예를 들어, @Service는 서비스 계층을 나타내고, @Repository는 데이터 엑세스 계층을 @Controller는 프레젠테이션 계층을 나타냅니다.
기능적으로는 동일하지만, 역할에 따라 어노테이션을 사용함으로써 코드의 가독성과 의미를 더 명확하게 할 수 있습니다.
5️⃣ 언제 사용해야 하는가?
일반적인 빈 등록이 필요할 때
비즈니스 로직, 유틸리티 클래스, 도메인 객체 등 Spring IoC 컨테이너에 의해 관리되어야 하는 일반적인 클래스를 빈으로 등록할 때 사용합니다.
특정 역할이 없는 클래스
@Service, @Repository, @Contoroller와 같은 명시적인 역할이 없는 경우, @Component를 사용하여 클래스를 빈으로 등록할 수 있습니다.
6️⃣ 결론.
@Component는 Spring 애플리케이션에서 자동으로 빈을 등록하는 데 사용되는 기본적인 어노테이션입니다.
이를 사용하면 Spring IoC 컨테이너가 클래스 경로에서 자동으로 해당 클래스를 찾아 빈으로 관리하게 됩니다.
일반적인 빈 등록을 위한 용도로 사용되며, 더 구체적인 역할을 나태내기 위해 @Service, @Repository, @Controller와 같은 특화된 어노테이션이 존재합니다.
@Component는 간단하게 빈을 관리하고자 할 때 유용하게 사용됩니다.
-
🍃[Spring] Spring 컨테이너를 사용하는 이유.
🍃[Spring] Spring 컨테이너를 사용하는 이유.
Spring 컨테이너를 사용하는 이유는 여러 가지가 있으며, 그 주요 목적은 객체의 생명주기와 의존성을 효율적으로 관리하고, 코드의 모듈화와 유연성을 높이는 데 있습니다.
Spring 컨테이너는 이를 통해 애플리케이션 개발을 단순화하고 유지보수성을 향상시키며, 코드의 재사용성과 테스트 가능성을 높입니다.
1️⃣ Spring 컨테이너를 사용하는 이유.
1. 의존성 주입(DI, Dependency Injection) 관리.
Spring 컨테이너는 객체 간의 의존선을 자동으로 주입해줍니다.
개발자는 객체를 직접 생성하고 연결할 필요 없이, 필요한 의존성을 외부에서 주입받도록 설정할 수 있습니다.
이로 인해 객체 간의 결합도가 낮아지고, 시스템이 더 유연해집니다.
예를 들어, 애플리케이션에서 사용하는 서비스나 리포지토리 간의 관계를 컨테이너가 자동으로 설정해 주기 때문에 객체 생성과 의존성 관리의 복잡성이 크게 줄어듭니다.
2. 객체의 생명주기 관리.
Spring 컨테이너는 객체(Bean, 빈)의 생성, 초기화, 사용, 소멸 등의 생명 주기를 관리합니다.
객체가 언제 생성되고, 언제 파괴되는지를 컨테이너가 자동으로 처리하므로 개발자가 객체의 상태를 일일이 관리할 필요가 없습니다.
이는 특히 싱글톤 빈을 사용하는 경우에 유용하며, 애플리게이션의 메모리 관리와 성능 최적화에도 기여합니다.
3. 모듈화와 재사용성.
Spring 컨테이너는 각 빈(Bean)을 독립적으로 관리하므로 모듈화된 코드를 쉽게 만들 수 있습니다.
객체의 생성과 초기화 로직이 분리되어 있으므로, 동일한 빈을 여러 곳에서 재사용할 수 있습니다.
또한, 의존성을 주입받아 사용하기 때문에 코드를 더 쉽게 재사용할 수 있습니다.
4. 테스트 용이성.
의존성 주입을 통해 객체 간의 결합도가 낮아지면, 단위 테스트가 훨씬 쉬워집니다.
Spring 컨테이너는 테스트 환경에서도 쉽게 사용할 수 있으며, Mock 객체를 주입해 실제 데이터베이스나 외부 시스템에 의존하지 않고도 각 객체의 동작을 검증할 수 있습니다.
이는 특히 자동화 테스트와 TDD(Test-Driven Development) 에서 큰 장점으로 작용합니다.
5. 애플리케이션 설정의 일관성.
Spring 컨테이너는 애플리케이션의 구성 요소를 일관되게 설정할 수 있는 환경을 제공합니다.
빈 설정을 XML, 자바 설정 클래스, 또는 어노테이션 기반으로 일관되게 관리할 수 있어, 애플리케이션의 구성 방식이 표준화됩니다.
이는 복잡한 애플리케이션에서도 설정 관리의 복잡성을 줄이는 데 큰 도움을 줍니다.
6. 애플리케이션 유연성 향상.
Spring 컨테이너는 의존성을 인터페이스로 추상화하고 구현체를 주입하기 때문에, 구현체의 변경이 매우 유연합니다.
예를 들어, 데이터베이스 저장소를 변경하거나 외부 서비스와 통신하는 방식이 바뀌더라도, 해당 구현제만 변경하면 다른 코드는 수정하지 않아도 되도록 설계할 수 있습니다.
7. AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍) 지원.
Spring 컨테이너는 AOP를 지원하므로, 비즈니스 로직과 관련 없는 횡단 관심사(로깅, 트랜잭션 관리, 보안 등)를 별도의 모듈로 분리할 수 있습니다.
이를 통해 코드를 더 모듈화할 수 있으며, 비즈니스 로직이 횡단 관심사로 인해 복잡해지는 것을 방지할 수 있습니다.
8. 트랜잭션 관리.
Spring 컨테이너는 트랜잭션 관리를 쉽게 설정할 수 있는 기능을 제공합니다.
트랜잭션 처리를 여러 곳에서 수동으로 관리할 필요 없이, 어노테이션을 통해 트랜잭션 경계를 정의하고 자동으로 관리되도록 설정할 수 있습니다.
이는 특히 데이터베이스와 관련된 작업에서 일관된 데이터 처리를 보장합니다.
9. 국제화 지원.
Spring 컨테이너는 애플리케이션의 국제화(i18n) 를 지원합니다.
이를 통해 다국어 지원이 필요한 애플리케이션에서 메시지 번역 및 포맷팅을 쉽게 처리할 수 있습니다.
2️⃣ 결론.
Spring 컨테이너를 사용하는 이유는 객체의 생명주기 관리와 의존성 주입을 자동화하여, 더 모듈화되고 유연한 코드를 작성할 수 있게 하기 위함입니다.
이를 통해 개발자는 객체 관리와 의존성 주입에 대한 복잡성을 줄이고, 비즈니스 로직 주현에 집중할 수 있습니다.
또한, Spring 컨테이너는 테스트 용이성, 유연성, 재사용성, 유지보수성 등을 크게 향상시켜 애플리케이션 개발의 효율성을 높이는 데 중요한 역할을 합니다.
-
-
🍃[Spring] Spring 컨테이너.
🍃[Spring] Spring 컨테이너.
Spring 컨테이너는 Spring Framework의 핵심 구성 요소로, 애플리케이션의 빈(Bean) 을 생성하고 관리하는 IoC(Inversion Of Control) 컨테이너를 의미합니다.
Spring 컨테이너는 애플리케이션이 동작하는 동안 객체(Bean, 빈)의 생명주기를 관리하며, 의존성 주입(Dependency Injection, DI) 을 통해 객체 간의 의존성을 자동으로 처리합니다.
1️⃣ Spring 컨테이너의 주요 역할.
1. 빈 생성 및 관리
Spring 컨테이너는 애플리케이션 내에서 필요한 빈을 생성하고 그 생명주기를 관리합니다.
빈의 생성, 초기화, 의존성 주입, 소멸의 모든 과정을 컨테이너가 제어합니다.
2. 의존성 주입(Dependency Injection)
컨테이너는 빈 간의 의존성을 분석하고, 필요한 경우 의존성을 자동으로 주입합니다.
이를 통해 개발자는 객체를 직접 생성하거나 연결할 필요 없이, 필요한 객체를 컨테이너가 주입해줍니다.
3. 빈 설정 및 구성.
Spring 컨테이너는 XML 파일, 자바 설정 클래스, 어노테이션 등을 통해 설정된 빈의 구성을 관리합니다.
설정 파일이나 어노테이션을 통해 각 빈이 어떤 다른 빈을 필요로 하는지 정의할 수 있습니다.
4. 빈의 생명주기 관리.
컨테이너는 빈의 생명주기(생성, 초기화, 사용, 소멸)를 제어합니다.
예를 들어, 애플리케이션이 시작될 때 컨테이너는 필요한 빈을 생성하고, 종료될 때 빈의 자원을 해제하는 등의 역할을 수행합니다.
5. 스코프 관리.
Spring 컨테이나는 빈의 스코프를 관리합니다.
싱글콘 스코프(애플리케이션 전체에서 하나의 인스턴스만 존재) 또는 프로토타입 스코프(요청마다 새로운 인스턴스 생성)등의 다양한 스코프를 지원합니다.
2️⃣ Spring 컨테이너의 유형.
Spring 컨테이너는 다양한 유형이 있으며, 이들은 모두 기본적으로 동일한 IoC 기능을 제공하지만, 사용 목적이나 구성이 다를 수 있습니다.
1. BeanFactory
Spring의 가장 기본적인 IoC 컨테이너입니다
지연 로딩(lazy loading) 을 사용하여 빈이 실제로 필요할 때까지 생성하지 않습니다.
이 방식은 리소스가 제한된 환경에서 유용하지만, 복잡한 해플리케이션에서는 거의 사용되지 않습니다.
2. ApplicationContext
Spring 컨테이너의 보다 확장된 형태로, 즉시 로딩(eager loading) 방식을 사용해 애플리케이션 시작시 빈을 미리 생성합니다.
ApplicationContext는 BeanFactory의 모든 기능을 포함하여, 추가적인 기능을 제공합니다.
이 컨테이너는 대부분의 Spring 애플리케이션에서 사용됩니다.
주요 구현체.
ClassPathXmlApplicationContext : XML 설정 파일을 사용해 애플리케이션 컨텍스트를 구성합니다.
AnnotationConfigApplicationContext : 자바 기반 설정을 사용해 애플리케이션 컨텍스트를 구성합니다.
WebApplicationContext : Spring MVC 애플리케이션에서 사용되는 컨테이너로, 웹 환경에 맞는 기능을 제공합니다.
3️⃣ Spring 컨테이너의 동작 과정.
1. 빈 정의 및 등록
애플리케이션에서 사용할 빈을 설정 파일(XML, 자바 클래스) 또는 어노테이션을 통해 정의합니다.
이 빈 정의는 Spring 컨테이너가 관리할 객체의 청사진 역할을 합니다.
2. 컨테이너 초기화
애플리케이션이 시작될 때 Spring 컨테이너가 초기화되며, 빈을 생성하고 의존성을 주입합니다.
이때 컨테이너는 설정에 따라 빈을 생성하고 빈 사이의 의존 관계를 설정합니다.
3. 빈 요청 및 사용
애플리케이션에서 빈이 필요할 때, 컨테이너에서 빈을 요청합니다.
컨테이너는 요청된 빈을 반환하며, 이 빈은 애플리케이션 내에서 사용됩니다.
4. 빈 소멸
애플리케이션이 종료되거나 빈이 더 이상 필요하지 않으면, 컨테이너는 빈의 소멸 메서드를 호출하여 빈을 적절히 정리합니다.
4️⃣ Spring 컨테이너의 예시
1. XML 설정 기반 컨테이너
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.example.UserService">
<property name="userRepository" ref="userRepository"/>
</bean>
<bean id="userRepository" class="com.example.UserRepository"/>
</beans>
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean(UserService.class);
2. 자바 설정 기반 컨테이너
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
}
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
3. 어노테이션 기반 설정
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
@ComponentScan(basePackages = "com.example")
@Configuration
public class AppConfig {}
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
5️⃣ 결론
Spring 컨테이너는 Spring 애플리케이션에서 객체(빈)의 생성, 관리, 의존성 주입 등을 자동으로 처리하는 핵심 인프라입니다.
이를 통해 개발자는 객체 생성 및 관리의 복잡성을 줄이고, 코드의 모듈화와 유연성을 높일 수 있습니다.
다양한 설정 방식(XML, 자바 설정, 어노테이션 등)을 통해 개발자는 컨테이너와 빈을 쉽게 구성하고 관리할 수 있습니다.
-
🍃[Spring] Spring 빈(Bean).
🍃[Spring] Spring 빈(Bean).
Spring에서 빈(Bean) 은 Spring IoC 컨테이너에 의해 관리되는 객체를 의미합니다.
간단히 말해, 빈은 스프링 애플리케이션의 핵심 구성 요소로, 개발자가 정의한 객체(클래스 인스턴스)가 Spring의 관리 하에 동작하는 것을 뜻합니다.
1️⃣ Spring 빈의 주요 개념.
1. 빈 정의(Bean Definition)
스프링 애플리케이션에서 빈은 개발자가 정의한 객체입니다.
빈은 일반적으로 애플리케이션의 중요한 서비스, 레포지토리, 컨트롤러 같은 객체들로, 스프링 컨테이너에 의해 생명주기가 관리됩니다.
2. 스프링 IoC(Inversion of Control) 컨테이너.
Spring IoC 컨테이너는 빈의 생성, 초기화, 설정, 소멸 등의 생명주기를 관리합니다.
개발자는 직접 객체를 생성하거나 소멸시키지 않고, IoC 컨테이너에 그 역할을 맡깁니다.
IoC 컨테이너는 빈을 필요에 따라 자동으로 주입하고 관리합니다.
3. 빈 등록.
빈은 XML 설정 파일, 자바 설정 클래스, 또는 어노테이션 기반으로 등록할 수 있습니다.
어노테이션 기반으로 빈을 등록하는 방식이 Spring Boot에서는 주로 사용됩니다.
2️⃣ 빈의 정의와 생성 방식.
1. 어노테이션 기반 빈 등록(Spring Boot에서 자주 사용)
빈을 생성하는 가장 일반적인 방법은 클래스에 어노테이션을 붙여 빈으로 등록하는 방식입니다.
@Component
일반적인 빈으로 등록할 때 사용됩니다.
@Sevice, @Repository, @Controller
각각 서비스, 레포지토리, 컨트롤러 역할을 하는 빈을 등록할 때 사용되는 더 구체적인 어노테이션입니다.
예시
@Component
public class MyService {
// 이 클래스는 스프링 컨테이너에 의해 관리되는 빈이 됩니다.
}
@Service
public class UserService {
// 이 클래스도 @Service로 빈으로 등록됩니다.
}
2. 자바 설정 클래스에서 빈 등록
자바 설정 파일을 사용해 명시적으로 빈을 등록할 수도 있습니다.
이 방식에서는 @Configuration 과 @Bean 어노테이션을 사용합니다.
예시
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
@Bean 어노테이션을 사용해 메서드가 반환하는 객체를 빈으로 등록할 수 있습니다.
3. XML 기반 설정(현재는 잘 사용되지 않음)
과거에는 XML 파일을 사용해 빈을 설정했으나, 현재는 주로 자바 설정과 어노테이션 기반 설정을 사용합니다.
3️⃣ 빈의 생명주기
스프링 IoC 컨테이너는 빈의 생명주기를 관리합니다.
빈의 생명주기는 다음과 같습니다.
1. 생성.
스프링 컨테이너가 빈을 생성합니다.
2. 의존성 주입.
생성된 빈에 필요한 의존성(다른 빈)이 주입됩니다.
3. 초기화.
빈이 필요한 설정 및 초기화 작업을 수행할 수 있습니다.
4. 사용.
빈이 애플리케이션 내에서 사용됩니다.
5. 소멸.
애플리케이션 종료 시 빈이 소멸됩니다.
3️⃣ 빈 스코프
스프링 빈은 여러 가지 스코프를 가질 수 있습니다.
스코프는 빈이 생성되고 사용되는 범위를 지정하는 것입니다.
1. 싱글톤(Singleton)
스프링 애플리케이션 내에서 기본 스코프로, 컨테이너에 한 번만 생성됩니다.
이후 같은 빈에 대한 요청이 있을 경우, 동일한 객체 인스턴스가 반환됩니다.
거의 대부분의 스프링 빈은 싱글톤으로 관리됩니다.
2. 프로토타입(Prototype)
빈이 요청될 때마다 새로운 인스턴스가 생성됩니다.
즉, 매번 다른 객체가 반환됩니다.
3. Request, Session, Application
웹 애플리케이션에서 사용되는 스코프로, 각각 HTTP 요청당, 세션당, 또는 서블릿 컨텍스트당 새로운 빈을 생성합니다.
4️⃣ 예시: 싱글톤 빈
@Service
public class UserService {
// 이 클래스는 싱글톤 빈으로 관리됩니다.
}
스프링 애플리케이션 내에서 UserService 빈은 한 번만 생성되고, 애플리케이션 전체에서 동일한 객체로 사용됩니다.
5️⃣ 결론
스프링 빈은 스프링 IoC 컨테이너에 의해 관리되는 객체로, 애플리케이션의 주요 구성 요소를 의미합니다.
빈은 어노테이션 또는 자바 설정 파일을 통해 등록할 수 있으며, 스프링 컨테이너는 빈의 생명주기를 자동으로 관리합니다.
이를 통해 의존성 주입, 객체 관리, 그리고 애플리케이션 전반의 유연성을 크게 향상시킬 수 있습니다.
-
🍃[Spring] 계층형 아키텍처에서 Service의 역할.
🍃[Spring] 계층형 아키텍처에서 Service의 역할.
Java 백엔드 애플리케이션의 계층형 아키텍처에서 Service 계층은 비즈니스 로직 을 처리하는 중간 계층입니다.
Service 계층은 Controller와 Repository 계층 사이에 위치하며, 비즈니스 규칙을 관리하고 데이터를 조작하는 역할을 수행합니다.
1️⃣ Service 계층의 주요 역할.
1. 비즈니스 로직 처리.
Service 계층은 애플리케이션의 핵심 비즈니스 로직을 처리합니다.
데이터를 단순히 전달하는 역할을 하는 Controller와 달리, Service는 복잡한 연산, 규칙 적용, 조건 판단 등의 작업을 수행합니다.
이를 통해 비즈니스 요구 사항을 충족하는 결과를 도출합니다.
2. 트랜잭션 관리.
Service 계층은 여러 데이터베이스 연산을 트랜잭션 단위로 묶어 관리할 수 있습니다.
예를 들어, 여러 데이터베이스 테이블에서 데이터를 읽거나 쓸 때 트랜잭션을 적용하여 모든 작업이 성공적으로 완료되거나, 문제가 생기면 롤백하는 등의 작업을 수행합니다.
Spring에서는 @Transactional 어노테이션을 통해 트랜잭션을 관리할 수 있습니다.
3. Repository 계층과 통신.
Service 계층은 Repository 계층을 사용해 데이터베이스와 상호작용합니다.
Service 계층은 비즈니스 로직에 필요한 데이터를 Repository에서 가져오거나 저장하는 작업을 수행합니다.
이렇게 함으로써, 비즈니스 로직과 데이터베이스 접근 로직을 분리해 코드를 더 깔끔하게 유지할 수 있습니다.
4. 다중 데이터 소스 처리.
Service 계층은 단일 데이터 소스가 아닌 여러 데이터 소스에 대한 조작을 중앙에서 관리할 수 있습니다.
예를 들어, 여러 데이터베이스에서 데이터를 조회하고 이를 결합하여 처리하는 등의 복잡한 작업을 수행할 수 있습니다.
5. 비즈니스 로직 재사용.
여러 Controller에서 동일한 비즈니스 로직이 필요할 경우, Service 계층에서 해당 로직을 구현하고 이를 여러 컨트롤러에서 재사용할 수 있습니다.
이를 통해 코드 중복을 방지하고, 로직을 단일화하여 유지보수성을 높일 수 있습니다.
6. 보안 및 검증 처리.
Service 계층은 추가적인 검증이나 보안 처리를 수행할 수 있습니다.
예를 들어, 사용자가 특정 데이터를 조회할 권한이 있는지 검증하거나, 입력된 데이터를 추가적으로 확인하는 작업을 포함할 수 있습니다.
7. 외부 시스템과의 통신.
Service 계층은 외부 API와의 통신, 메일 발송, 메시지 큐 처리 등 비즈니스 로직을 수행하기 위해 다른 시스템이나 서비스와 상호작용하는 역할도 담당합니다.
2️⃣ 예시 코드
Spring Boot 애플리케이션에서의 Service 계층 예시를 통해 그 역할을 구체적으로 살펴볼 수 있습니다.
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository
}
@Transactional
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id " + id));
return new UserDTO(user);
}
@Transactional
public UserDTO createUser(UserDTO userDTO) {
User user = new User(userDTO.getName(), userDTO.getEmail());
User savedUser = userRepository.save(user);
return new UserDTO(savedUser);
}
@Transactional
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
thorw new UserNotFoundException("User not found with id " + id);
}
userRepository.deleteById(id);
}
}
3️⃣ 설명.
@Service
Spring에서 서비스 클래스임을 나타내는 어노테이션입니다.
이 클래스는 비즈니스 로직을 처리하는 곳 입니다
@Transactional
메서드가 트랜잭션 안에서 실행되도록 보장합니다.
여러 데이터베이스 연산이 트랜잭션 단위로 처리되며, 오류 발생 시 롤백됩니다.
비즈니스 로직
getUserById 메서드는 사용자가 존재하지 않으면 예외를 던지를 로직을 포함하고 있으며, createUser는 사용자 객체를 생성하고 이를 저장한 후 다시 반환하는 로직을 처리합니다.
Repository와 통신
Service는 Repository 계층을 사용해 데이터베이스에 접근하여 사용자 데이터를 가져오거나 저장합니다.
4️⃣ 결론.
Service 계층은 애플리케이션의 핵심 비즈니스 로직을 캡슐화하고 관리하는 역할을 합니다.
이를 통해 비즈니스 로직을 쉽게 유지하고 재사용할 수 있으며, Controller와 데이터 접근 계층(Repository) 간의 명확한 분리를 유지하여 시스템의 유연성과 유지보수성을 높입니다.
-
🍃[Spring] 계층형 아키텍처에서 Repository의 역할.
🍃[Spring] 계층형 아키텍처에서 Repository의 역할.
Java 백엔드 애플리케이션의 계층형 아키텍처에서 Repository 계층은 데이터베이스와의 상호작용을 관리하는 역할을 합니다.
Repository는 데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리하며, 데이터를 저장소(일반적으로 데이터베이스)에서 가져오고, 수정하며, 삭제하는 기능을 캡슐화합니다.
이를 통해 애플리케이션의 비즈니스 로직에서 데이터 접근을 분리하고, 유지보수성과 테스트 가능성을 높입니다.
1️⃣ Repository 계층의 주요 역할.
1. 데이터베이스 접근 관리.
Repository는 애플리케이션과 데이터베이스 간의 중개자 역할을 하며, 데이터베이스로부터 데이터를 조회하거나, 데이터를 저장, 수정, 삭제하는 기능을 제공합니다.
모든 데이터 접근 로직은 Repository 계층에서 처리됩니다.
2. CRUD 작업 처리.
Repository는 엔티티의 생명주기 전반에 걸친 CRUD 작업을 처리합니다.
Java의 JPA나 Hibernate 같은 ORM(Object-Relational Mapping) 프레임워크를 사용해 객체와 데이터베이스 간의 매핑을 자동으로 관리합니다.
예를 들어, save, findById, deleteById 등의 메서드를 통해 객체를 데이터베이스에 저장하거나 조회하는 작업을 수행합니다.
3. 데이터 쿼리 처리.
Repository 계층은 데이터를 조회하기 위해 데이터베이스에서 쿼리를 생성하고 실행합니다.
Spring Data JPA와 같은 프레임워크를 사용하면 쿼리 메서드를 간편하게 정의할 수 있으며, 복잡한 조건 검색을 위한 JPQL(Java Persistence Query Language) 또는 네이티브 SQL 쿼리를 사용할 수 있습니다.
기본적인 쿼리 메서드 외에도 복잡한 쿼리를 작성하여 특정 조건에 맞는 데이터를 필터링할 수 있습니다.
4. 데이터베이스와의 추상화.
Repository는 데이터베이스 접근 로직을 캡슐화하여 상위 계층(Service 등)에서 데이터베이스의 구체적인 동작 방식을 알 필요가 없도록 합니다.
이렇게 하면 데이터베이스가 변경되더라도 상위 계층에는 영향을 미치지 않도록 구조를 유지할 수 있습니다.
5. 데이터베이스 독립성.
Repository를 사용함으로써 데이터베이스와의 구체적인 종속성을 줄일 수 있습니다.
애플리케이션이 사용하는 데이터베이스가 변경되더라도, Repository 계층만 적절히 수정하면 상위 계층에는 영향을 주지 않기 때문에, 데이터베이스 독립성을 유지할 수 있습니다.
6. 객체와 데이블 매핑 관리
ORM을 사용해 데이터베이스 테이블과 객체를 매핑하는 역할을 담당합니다.
객체와 테이블 간의 관계(1, N)를 정의하고, 이러한 관계를 기반으로 데이터베이스 연산을 수행합니다.
2️⃣ 예시 코드
Spring Data JPA를 사용한 Repository 계층의 예시를 살펴보겠습니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 기본적으로 JpaRepository에서 제공하는 CRUD 메서드들
// save, fundById, findAll, deleteById 등이 제공됩니다.
// 커스텀 쿼리 메서드 정의
Optional<User> findByEmail(String email);
// 복잡한 JPQL 쿼리 메서드 정의 가능
@Query("SELECT u FROM User u WHERE u.name = :name AND u.status = :status")
List<User> findByNameAndStatus(@Param("name") String name, @Param("status") String status);
}
3️⃣ 설명
@Repository
Spring에서 이 인터페이스가 데이터 엑세스 계층을 나타낸다는 것을 명시하는 어노테이션입니다.
@Repository 어노테이션은 데이터베이스 예외를 Spring의 데이터 엑세스 예외로 변환하는 역할도 합니다.
JpaRepository<User, Long>
Spring Data JPA에서 제공하는 기본 인터페이스로, User 엔티티와 Long 타입의 ID를 사용해 CRUD 작업을 처리합니다.
기본 CRUD 메서드
save, findById, deleteById 등의 메서드가 기본적으로 제공됩니다.
커스텀 쿼리 메서드
매서드 명명 규칙을 통해 자동으로 SQL 쿼리를 생성하는 방식으로 특정 필드로 검색하는 메서드를 정의할 수 있습니다.
예를 들어, findByEmail은 이메일을 기준으로 사용자를 조회합니다.
JPQL 사용
복잡한 조건이 필요한 경우, @Query 어노테이션을 사용해 JPQL을 작성할 수 있습니다.
이 예시에서는 이름과 상태를 기반으로 사용자를 조회하는 쿼리를 정의했습니다.
4️⃣ Repository 계층의 이점.
1. 비즈니스 로직과 데이터 접근 로직의 분리.
Repository 계층은 비즈니스 로직과 데이터베이스 접근을 명확히 분리하여 각 계층이 자신의 역활에만 집중할 수 있게 합니다.
이렇게 하면 코드가 더 모듈화되고 유지보수하기 쉬워집니다.
2. 코드의 재사용성.
Repository 계층은 데이터베이스 접근 로직을 재사용할 수 있도록 만들어집니다.
여러 서비스에서 동일한 데이터베이스 쿼리나 데이터를 필요로 할 때, 이를 중앙에서 관리함으로써 코드 중복을 줄일 수 있습니다.
3. 테스트 가능성 향상.
Repository 계층을 인터페이스로 정의함으로써 테스트 시 Mock 객체로 쉽게 대체할 수 있어, 데이터베이스에 접근하지 않고도 단위 테스트를 작성할 수 있습니다.
4. 데이터베이스 변경의 유연성.
데이터베이스가 변경되더라도 Repository 계층만 수정하면 상위 계층(Service나 Controller)은 전혀 변경하지 않고도 애플리케이션을 유지할 수 있습니다.
데이터베이스 독립성을 높이는 데 중요한 역할을 합니다.
5️⃣ 결론
Repository 계층은 데이터베이스와 상호작용하는 모든 작업을 담당하며, 데이터를 저장, 조회, 업데이트, 삭제하는 기능을 캡슐화합니다.
이를 통해 데이터 접근 로직이 비즈니스 로직과 분리되므로 애플리케이션의 유지보수성, 확장성, 재사용성이 크게 향상됩니다.
-
🍃[Spring] 의존성(Dependency).
🍃[Spring] 의존성(Dependency).
Java 백엔드 애플리케이션에서 의존성(Dependency) 이란 한 클래스 또는 모듈이 다른 클래스 또는 모듈의 기능을 필요로 하거나, 그 존재에 따라 동작하는 관계를 의미합니다.
간단히 말해, 의존성은 어떤 클래스가 다른 클래스에 의존하여 해당 클래스의 메서드나 기능을 호출하고 사용하는 것을 말합니다.
의존성은 객체 지향 프로그래밍에서 자연스럽게 발생하는 관계로, 특정 객체를 필요로 할 때 발생합니다.
예를 들어, 서비스 클래스가 데이터베이스와 상호작용하기 위해 리포지토리 클래스에 의존하거나, 컨트롤러가 비즈니스 로직을 수행하기 위해 서비스 클래스에 의존하는 경우가 해당됩니다.
1️⃣ 의존성의 예시
간단한 의존성 예
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
UserService 클래스는 UserRepository 클래스에 의존합니다.
UserService는 비즈니스 로직을 처리하기 위해 UserRepository의 메서드를 호출합니다.
만약 UserRepository가 없으면 UserService는 정상적으로 작동할 수 없습니다.
따라서, 이 두 클래스 간에는 의존성이 존재합니다.
2️⃣ 의존성 관리의 중요성.
의존성(Dependency)은 자연스럽게 발생하지만, 의존성을 관리하지 않으면 애플리케이션이 결합도가 높아지고, 변경에 취약해지며, 유지보수가 어려워질 수 있습니다.
따라서 의존성을 적절히 관리하는 것이 중요합니다.
의존성 관리의 좋은 방법으로는 의존성 주입(Dependency Injection, DI) 이 있습니다.
3️⃣ 의존성 주입(Dependency Injection, DI)
의존성 주입(Dependency Injection, DI)은 객체 간의 의존성을 외부에서 주입해주는 디자인 패턴으로, 의존성 주입을 통해 클래스는 자신이 사용할 객체를 직접 생성하지 않고, 외부에서 생성된 객체를 주입받아 사용하게 됩니다.
이를 통해 클래스 간의 결합도를 낮추고, 유연성과 테스트 가능성을 높일 수 있습니다.
의존성 주입 예시(Spring Framework)
Spring Framework에서 의존성 주입은 매우 흔하게 사용됩니다.
Spring은 객체 간의 의존성을 관리하고, 필요한 객체를 자동으로 주입해 줍니다.
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
@Autowired
Spring이 UserRepository의 구현체를 자동으로 주입해줍니다.
개발자는 UserService가 UserRepository에 의존한다는 사실만 명시하면 되고, UserRepository 객체의 생성을 직접적으로 관리할 필요가 없습니다.
4️⃣ 의존성의 종류.
1. 강한 의존성 (Tight Coupling)
클래스 A가 클래스 B의 구체적인 구현에 의존할 때 발생합니다.
이 경우 클래스 B가 변경되면 클래스 A도 수정이 필요할 수 있습니다.
예를 들어, new 키워드를 사용해 직접 객체를 생성하면 강한 의존성이 발생합니다.
2. 약한 의존성(Loose Coupling)
클래스 A가 클래스 B의 구체적인 구현이 아닌, 인터페이스나 추상 클래스에 의존할 때 발생합니다.
이는 결합도를 낮추어 클래스 간의 변경에 더 유연하게 대처할 수 있게 합니다.
의존성 주입을 통해 약한 의존성을 실현할 수 있습니다.
5️⃣ 의존성 관리의 이점.
1. 유지보수성 향상.
의존성이 적절히 관리되면, 코드가 더 모듈화되고 변경 사항이 발생할 때 수정해야 할 부분이 줄어듭니다.
예를 들어, 특정 클래스의 구현을 변경하더라도 의존성 주입을 통해 인터페이스만 유지하면 다른 클래스에는 영향을 주지 않습니다.
2. 테스트 가능성.
의존성을 외부에서 주입받으면, 테스트 시에 Mock 객체를 주입하여 쉽게 단위 테스트를 수행할 수 있습니다.
이를 통해 더 작은 단위 테스트가 가능해지고, 테스트가 독립적으로 수행될 수 있습니다.
3. 재사용성 향상.
클래스가 다른 클래스에 강하게 의존하지 않으면, 해당 클래스를 다른 곳에서도 재사용하기 쉽습니다.
인터페이스를 사용하고, 의존성 주입을 통해 다양한 구현체를 주입받아 사용할 수 있기 때문입니다.
6️⃣ 결론.
Java 백엔드 애플리케이션에서 의존성은 클래스 간의 관계를 의미하며, 이를 잘 관리하는 것이 중요합니다.
의존성 주입을 사용하면 결합도를 낮추고 유연성을 높일 수 있으며, 코드의 유지보수성과 테스트 가능성을 크게 향상 시킬 수 있습니다.
Spring과 같은 프레임워크에서는 이러한 의존성 관리와 주입을 매우 쉽게 처리할 수 있도록 지원하고 있습니다.
-
🍃[Spring] 계층형 아키텍처에서 Controller의 역할.
🍃[Spring] 계층형 아키텍처에서 Controller의 역할.
Java 백앤드 애플리케이션의 계층형 아키텍처(Layerd Architecture) 에서 Controller는 사용자(클라이언트)로부터 요청을 받아 처리하고, 그에 대한 응답을 반환하는 역할을 담당하는 중요한 계층입니다.
Controller는 애플리케이션의 외부 인터페이스로 작동하며, 주로 웹 요청과 응답의 흐름을 제어하는 역할을 수행합니다.
1️⃣ Controller의 주요 역할.
1. 요청 수신 및 응답 반환.
Controller는 클라이언트(브라우저, 모바일 앱 등)로 부터 HTTP 요청을 수신합니다.
각 요청은 URL 경로 및 HTTP 메서드(GET, POST, PUT, DELETE 등)에 따라 Controller의 특정 메서드에 매핑됩니다.
이 메서드는 비즈니스 로직을 호출하고, 처리 결과를 다시 클라이언트에게 응답으로 반환합니다.
2. 요청 검증.
Controller는 클라이언트로부터 전달된 요청 데이터(쿼리 파라미터, 폼 데이터, JSON 등)를 검증하는 역할을 합니다.
이 검증 작업은 일반적으로 요청이 비즈니스 로직으로 전달되기 전에 수행됩니다.
검증 실패 시, 에러 메시지나 상태 코드를 클라이언트에 반환합니다.
3. 비즈니스 로직 호출.
Controller는 직접적으로 비즈니스 로직을 수행하지 않고, Service 계층의 메서드를 호출합니다.
이 방식은 역할을 분리하여 유지보수성을 높이고, 코드가 더욱 이해하기 쉽게 만듭니다.
Service 계층이 비즈니스 로직을 처리하고 결과를 반환하면, Controller는 이를 클라이언트에게 전달합니다.
4. 뷰와의 통신(View와 연동)
Controller는 View와 통신하여 적절한 응답을 생성합니다.
웹 애플리케이션에서는 HTML이나 JSON 데이터를 반환하는 것이 일반적입니다.
예를 들어, 템플릿 엔진(Thymeleaf 등)을 사용해 동적인 HTML 페이지를 렌더링하거나, API 서버의 경우 JSON 포맷으로 데이터를 반환합니다.
5. HTTP 상태 코드 관리.
Controller는 요청 처리 결과에 따라 적절한 HTTP 상태 코드(예: 200 OK, 400 Bad Request, 404 Not Found, 500 Internal Server Error 등) 를 설정하여 클라이언트에 전달합니다.
이로써 클라이언트가 요청 처리 상태를 알 수 있도록 합니다.
6. 예외 처리
Controller는 애플리케이션에서 발생하는 예외를 처리하거나, 전역적인 예외 처리 메커니즘을 사용해 예외를 다룹니다.
이를 통해 적절한 에러 응답을 클라이언트에게 반환하고, 에러가 발생했을 때 애플리케이션이 비정상적으로 종료되지 않도록 합니다.
2️⃣ 예시 코드
Spring Boot 애플리케이션에서의 Controller 예시를 통해 그 역할을 더 구체적으로 볼 수 있습니다.
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody @Valid UserDTO userDTO) {
UserDTO createdUser = userService.createUser(userDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
}
3️⃣ 설명.
@RestController
Spring에서 RESTful 웹 서비스를 만들기 위해 사용되는 어노테이션입니다.
JSON 또는 XML 데이터를 반환하는 컨트롤러를 정의합니다.
@RequestMapping
이 컨트롤러가 \users 경로에 대한 요청을 처리하도록 설정합니다.
@GetMapping
HTTP GET 요청을 처리하는 메서드로, id 경로 변수에 해당하는 사용자를 가져옵니다.
@PostMapping
HTTP POST 요청을 처리하는 메서드로, 새로운 사용자를 생성하고 결과를 반환합니다.
@RequestBody, @Valid
요청 본문 데이터를 객체로 매핑하고, 입력 데이터를 검증합니다.
ResponseEntity
응답 본문과 상태 코드를 포함해 클라이언트에게 응답을 반환합니다.
4️⃣ 결론
Contoller는 사용자 요청을 받아 Service 계층과 통신하며, 요청을 처리하고 그 결과를 클라이언트에게 반환하는 역할을 합니다.
이러한 역할 분리를 통해 애플리케이션은 더욱 유연하고 관리하기 쉬운 구조를 갖추게 됩니다.
-
💾 [CS] 좋은 코드(Clean Code)의 중요성.
💾 [CS] 좋은 코드(Clean Code)의 중요성.
클린 코드(Clean Code)는 읽기 쉽고 이해하기 쉬우며 유지 보수하기 쉬운 코드를 의미합니다.
소프트웨어 개발에서 클린 코드는 단순히 기능하는 코드를 넘어서, 일관성, 가독성, 명확성, 그리고 간결성을 갖춘 코드를 작성하는 것을 목표로 합니다.
클린 코드는 그 자체로 직관적이어야 하며, 개발자가 코드의 동작을 쉽게 파악할 수 있게 도와줍니다.
1️⃣ 클린 코드의 특징.
1. 명확한 목적과 이름.
클래스, 메서드, 변수 이름이 그 목적을 명확하게 드러내야 합니다.
이름만 보고도 해당 코드가 무엇을 하는지 쉽게 파악할 수 있어야 합니다.
2. 단순함(Simplicity)
코드가 불필요하게 복잡하지 않아야 하며, 가장 단순한 방법으로 문제를 해결하려고 해야 합니다.
단순한 코드는 이해하기 쉽고, 오류가 발생할 가능성이 줄어듭니다.
3. 일관성(Consistency)
코드 스타일과 구조는 일관성이 있어야 합니다.
이로 인해 코드를 읽는 사람이 새로운 규칙을 학습할 필요 없이 일관된 형식을 따라갈 수 있습니다.
4. 작은 함수(Small Function)
함수나 메서드는 하나의 작업만을 수행하고, 그 작업을 명확하게 나타내야 합니다.
함수는 작고 간결해야 하며, 너무 많은 일을 하거나 너무 많은 책임을 가져서는 안됩니다.
5. 의존성 최소화(Low Coupling)
모듈 간의 의존성을 최소화하여, 한 부분이 변경되더라도 다른 부분에 미치는 영향을 최소화해야 합니다.
이로 인해 유지 보수와 확장이 쉬워집니다.
6. 중복 코드 제거(DRY: Don’t Repeat Yourself)
동일한 코드가 여러 곳에서 반복되지 않도록 해야 합니다.
중복 코드는 버그 발생 가능성을 높이고 유지 보수를 어렵게 만듭니다.
7. 에러 처리 및 예외 처리 명확성.
클린 코드는 예상치 못한 상황에 대한 처리가 명확하게 이루어져야 합니다.
에러나 예외가 발생했을 때 그 흐름이 한 눈에 이해될 수 있도록 작성되어야 합니다.
8. 주석은 최소화하되, 필요한 경우에는 설명적으로
클린 코드는 그 자체로 충분히 설명적이기 때문에 주석이 많이 필요하지 않습니다.
대신, 주석은 코드가 아닌 복잡한 비즈니스 로직이나 의도에 대한 설명으로 사용되어야 합니다.
2️⃣ 클린 코드를 지향하는 원칙.
1. SOLID 원칙.
객체지향 설계의 5원칙(단일 책임 원칙, 개방-폐쇠 원칙, 리스코프 치환 원칙, 인터페이스 분리 원칙, 의존성 역전 원칙)을 지쳐, 더 나은 구조와 확장성을 갖춘 코드를 작성합니다.
🙋♂️ SOLID 원칙
2. KISS 원칙(Keep It Simple, Stupid)
코드는 가능한 한 간단하고 복잡하지 않기 유지되어야 한다는 원칙입니다.
3. YAGNI 원칙(You Ain’t Gonna Need It)
필요하지 않은 기능을 미리 구현하지 않고, 필요할 때만 추가하는 원칙입니다.
코드가 지나치게 복잡해지는 것을 방지합니다.
4. DRY 원칙(Don’t Repeat Yourself)
중복을 피하고, 같은 로직을 반복하지 않도록 코드를 설계하는 원칙입니다.
3️⃣ 클린 코드의 중요성
유지 보수성 : 시간이 지나도 이해하기 쉽고 수정하기 쉬운 코드는 애플리케이션의 수명을 연장합니다.
버그 감소 : 명확하고 간결한 코드는 버그가 발생할 가능성을 줄입니다.
협업 : 여러 개발자가 협업할 때, 클린 코드는 의사소통을 원활하게 하며 코드 리뷰 과정도 쉽게 만듭니다.
확장성 : 클린 코드는 확장과 변화에 유연하게 대처할 수 있어 새로운 기능을 추가하는 데 용이합니다.
클린 코드는 단순히 코드 스타일이 아니라, 소프트웨어의 품질을 높이는 데 필수적인 철학과 원칙입니다.
4️⃣ Java 백엔드 애플리케이션에서 클린 코드가 중요한 이유.
Java 백엔드 애플리케이션에서 클린 코드가 중요한 이유는 여러 가지가 있습니다.
이를 통해 개발자의 생산성과 유지 보수성이 크게 향상되며, 시스템의 안정성과 확장성도 개선됩니다.
1. 이해와 가독성 향상.
클린 코드는 코드를 읽는 사람이 쉽게 이해할 수 있도록 작성됩니다.
Java와 같은 객체지향 언어에서는 클래스, 메서드, 변수 이름이 직관적이고 목적에 맞게 명명되어야 합니다.
이를 통해 다른 개발자가 코드를 분석하고 변경 사항을 반영하는 데 드는 시간이 줄어듭니다.
2. 유지 보수 용이.
클린 코드는 명확하고 일관성 있게 작성되어, 유지 보수 시 복잡한 로직을 쉽게 파악할 수 있습니다.
불필요한 코드나 중복 코드를 제거하고, 모듈화된 구조를 유지하면 변경 사항이 발생하더라도 특정 부분만 수정하면 되기 때문에 유지 보수가 수월해 집니다.
3. 버그 발생 확률 감소.
가독성이 떨어지는 복잡한 코드는 종종 버그를 숨깁니다.
클린 코드는 명확하고 일관성이 있어, 개발자들이 실수를 줄일 수 있습니다.
또한, 잘 작성된 테스트 코드와 결합하면 버그가 발생할 확률이 더욱 낮아집니다.
4. 확장성과 재사용성 증가.
클린 코드는 원칙에 맞게 모듈화되고, 단일 책임 원칙(Single Responsibility Principle), 개방-폐쇄 원칙(Open/Closed Principle) 등과 같은 객체지향 설계 원칙들을 따릅니다.
이를 통해 코드의 확장성과 재사용성이 높아져, 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 쉽게 확장할 수 있습니다.
5. 협업 개선.
여러 명의 개발자가 참여하는 백엔드 프로젝트에서는 코딩 스카일의 일관성이 매우 중요합니다.
클린 코드 원칙을 따르면, 팀원 간에 코드를 공유하고 협업할 때 더 원활한 의사소통이 가능해집니다.
코드 리뷰도 보다 쉽게 진행할 수 있습니다.
6. 테스트 용이성.
클린 코드는 테스트를 작성하기 쉽게 만듭니다.
메서드가 간결하고 명확하면 단위 테스트(Unit Test)를 작성하는 것이 더 쉬워지고, 이를 통해 애플리케이션의 안정성을 높일 수 있습니다.
테스트 가능성이 높은 코드는 품질 관리에 매우 유리합니다.
결론적으로, 클린 코드는 장기적으로 백엔드 애플리케이션의 안정성, 확장성, 유지 보수성을 높이며, 팀원들 간의 협업을 촉진하고 버그 발생 가능성을 줄여 개발 효율성을 크게 향상시킵니다.
-
💾 [CS] SOLID 원칙.
💾 [CS] SOLID 원칙.
SOLID 원칙은 객체지향 프로그래밍에서 유지보수성과 확장성이 좋은 소프트웨어 설계를 위해 제안된 5가지 중요한 설계 원칙입니다.
이 원칙들을 따르면 코드가 더 구조화되고, 관리하기 쉬워지며, 유연하게 변화에 대처할 수 있습니다.
SOLID 원칙은 다음과 같은 5가지 원칙의 첫 글자를 딴 약어 입니다.
1️⃣ 단일 책임 원칙(SRP: Single Responsibility Principle)
정의
클래스는 단 하나의 책임만 가져야 하며, 하나의 기능만 담당해야 합니다.
설명
클래스를 변경할 이유가 하나뿐이어야 한다는 의미입니다.
즉, 클래스가 한 가지 역할에 집중하고 그 역할과 관련된 책임만을 가져야 합니다.
이렇게 클래스가 명확하고 이해하기 쉬워지며, 코드 수정이 필요할 때 영향을 받는 범위가 작아집니다.
예시
게시글을 관리하는 클래스가 게시글 저장, 수정, 삭제 등 여러 가지 역할을 맡는 대신, 각 역할을 별도의 클래스로 나누는 방식입니다.
2️⃣ 개방-폐쇄 원칙(OCP: Open/Close Principle)
정의
클래스는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 합니다.
설명
새로운 기능을 추가할 때 기존의 코드는 변경하지 않고도 시스템을 확장할 수 있어야 합니다.
이를 위해 상속이나 인터페이스를 활용해 기존 클래스의 기능을 확장하는 방식이 권장됩니다.
예시
새로운 기능이 필요할 때 기존 코드를 수정하지 않고, 해당 기능을 상속받거나 인터페이스를 구현하는 방식으로 추가하는 것이 개방-폐쇄 원칙에 맞는 설계입니다.
3️⃣ 리스코프 치환 원칙(LSP: Liskov Substitution Principle)
정의
서브타입(하위 클래스)은 언제나 자신의 기반 타입(상위 클래스)으로 교체할 수 있어야 합니다.
설명
하위 클래스는 상위 클래스의 규약을 준수해야 하며, 상위 클래스를 사용하는 코드가 하위 클래스로 대체되더라도 정상적으로 동작해야 합니다.
이는 다형성(polymorphism) 개념을 따르는 객체지향 설계의 중요한 원칙입니다.
예시
만약 Animal이라는 상위 클래스가 있고, Bird와 Fish라는 하위 클래스가 있다면, Bird와 Fish는 Animal 타입으로 대체되어도 문제가 없어야 합니다.
4️⃣ 인터페이스 분리 원칙(ISP: Interface Segregation Principle)
정의
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
설명
하나의 큰 인터페이스보다 여러 개의 작은 인터페이스로 분리하는 것이 더 바람직합니다.
이렇게 하면 클라이언트는 자신이 필요로 하는 기능만 선택적으로 사용할 수 있습니다.
큰 인터페이스는 사용하지 않는 메서드까지 구현해야 하므로 유지보수가 어려워질 수 있습니다.
예시
다양한 작업을 수행하는 Workable 인터페이스를 여러 개 작은 인터페이스(Runnale, Flyable, Swimmable)로 분리하여 필요한 기능만 구현하도록 하는 것이 인터페이스 분리 원칙에 맞는 설계입니다.
5️⃣ 의존성 역전 원칙(DIP: Dependaency Inversion Principle)
정의
고수준 모듈은 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화된 것에 의존해야 합니다.
설명
고수준 모듈(비즈니스 로직)은 저수준 모듈(세부 구현)과 직접적으로 연결되지 않고, 추상화된 인터페이스를 통해 서로 상호작용해야 합니다.
이를 통해 변화에 유연하게 대처할 수 있으며, 코드가 더 확장 가능해집니다.
또한, 코드의 재사용성도 높아집니다.
예시
서비스 계층이 저장소 계층에 의존하지 않고, 저장소 계층을 추상화한 인터페이스에 의존하도록 설계하는 방식이 의존성 역전 원칙에 해당합니다.
이를 통해 저장소 계층의 구현체를 교체하더라도 서비스 계층에는 영향을 주지 않게 됩니다.
6️⃣ SOLID 원칙의 중요성
SOLID 원칙은 유연하고 유지보수하기 쉬운 시스템을 만들기 위한 기본 가이드 라인을 제공합니다.
이 원칙들을 따름으로써 소프트웨어는 다음과 같은 이점을 얻습니다.
1. 변화에 유연함.
새로운 요구 사항이나 기능이 추가되더라도 기존 코드를 최소한으로 수정하거나 전혀 수정하지 않고도 시스템을 확장할 수 있습니다.
2. 테스트 용이성.
코드의 모듈화가 잘 이루어지면 테스트 코드 작성이 더 쉬워지고, 개별적인 유닛 테이스가 가능해집니다.
3. 코드 재사용성.
SOLID 원칙에 따라 작성된 코드는 재사용 가능성이 높습니다.
4. 협업 용이성.
여러 개발자가 협업할 때 서로의 코드를 이해하고 확장하기가 더 쉬워집니다.
이 원칙들은 객체지향 설계의 모범 사례를 제시하며, 코드를 더 효율적으로 관리하고 확장하는 데 큰 도움을 줍니다.
-
🍃[Spring] API에서 예외 던지기.
🍃[Spring] API에서 예외 던지기.
Java 백엔드 애플리케이션에서 API 호출 중 예외를 던지면, 예외가 적절하게 처리되지 않는 한 클라이언트에 오류 응답이 반환됩니다.
기본적으로 예외가 발생하면 애플리케이션은 예외가 발생한 시점에서 실행을 중단하고, 예외에 대한 처리를 하지 않으면 내부 서버 오류(HTTP 500)를 클라이언트에 반환하게 됩니다.
1️⃣ 예외 처리 흐름.
1. 예외 발생.
API의 컨트롤러나 서비스 레이어에서 특정 로직을 수행하는 도중 예외가 발생할 수 있습니다.
예외가 발생하면 실행 흐름이 예외 처리 블록으로 넘어가거나, 예외가 호출된 메서드를 통해 상위로 전파됩니다.
2. 예외 전파.
만약 해당 예외를 try-catch 블록 내에서 처리하지 않으면 예외는 호출된 메서드를 따라 상위로 전파됩니다.
이 과정에서 최종적으로 컨트롤러 레벨이나 그 이상에서 예외를 잡지 않으면, Spring Framework와 같은 백엔드 프레임워크가 예외를 받아 처리하게 됩니다.
3. Spring의 기본 동작.
Spring MVC에서는 예외가 전파되어 컨트롤러까지 도달하고도 처리되지 않으면, Spring이 DefaultHandlerExceptionResolver를 통해 기본적인 예외 처리를 수행합니다.
처리되지 않은 예외는 기본적으로 HTTP 상태 코드 500(내부 서버 오류)로 매핑되어 클라이언트에 반환됩니다.
클라이언트는 이 상태 코드를 통해 서버에서 오류가 발생했음을 인식하게 됩니다.
2️⃣ 예외 처리를 위한 방법.
1. @ExceptionHandler 사용.
Spring MVC에서는 컨트롤러 레벨에서 예외를 처리하기 위해 @ExceptionHandler 어노테이션을 사용할 수 있습니다.
특정 예외가 발생했을 때 해당 메서드가 호출되어 예외를 처리하고, 적절한 응답을 클라이언트에게 반환할 수 있습니다.
@RestController
public class MyController {
@GetMapping("/example")
public String example() {
if (someConditionFails()) {
throw new MyCustomException("Something went wrong!");
}
return "success";
}
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<String> handleMyCustomException(MyCustomException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
이 경우 MyCustomException이 발생하면 HTTP 상태 코드 400과 함께 예외 메시지가 반환됩니다.
2. @ControllerAdvice 사용.
@ControllerAdvice는 전역 예외 처리기를 정의하는 데 사용됩니다.
이를 통해 애플리케이션 전반에 발생하는 예외를 중앙에서 처리할 수 있습니다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<String> handleMyCustomException(MyCustomException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception ex) {
return new ResponseEntity<>("An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
이 경우 특정 예외뿐만 아니라 모든 일반적인 예외도 처리할 수 있습니다.
3. ResponseEntity와 함께 예외를 반환.
ResponseEntity를 사용하여 직접 예외 발생 시 HTTP 응답과 상태 코드를 반환할 수도 있습니다.
@GetMapping("/example")
public ResponseEntity<String> example() {
if (someConditionFails()) {
return new ResponseEntity<>("Custom error message", HttpStatus.BAD_REQUEST);
}
return new ResponseEntiry<>("success", HttpStatus.OK);
}
3️⃣ 실제 코드 사례.
// UserUpdateRequest - DTO
public class UserUpdateRequest {
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
}
// UserController - Controller
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
String sql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, request.getName(), request.getId());
}
@DeleteMapping("/user")
public void deleteUser(@RequestParam String name) {
String sql = "DELETE FROM user WHERE name = ?";
jdbcTemplate.update(sql, name);
}
1. 문제 상황.
위 코드에서의 문제 상황은 없는 유저를 업데이트 하거나 삭제하려 해도 200 OK가 나온다는 점입니다.
2. 해결 방안.
API에서 예외를 던저 500 Internal Server Error이 나오게 하면됩니다.
@GetMapping("/user/error-test")
public void errorTest() {
throw new IllegalArgumentException();
}
POSTMAN을 활용하여 http://localhost:8080/user/error-test로 GET 호출을 보내면 500 Internal Server Error이 발생합니다.
4️⃣ 활용 예시.
// UserUpdateRequest - DTO
public class UserUpdateRequest {
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
}
// UserController - Controller
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
String readSql = "SELECT * FROM user WHERE id = ?";
boolean isUserExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0 request.getId()).isEmpty();
if (isUserNotExist) {
throw new IllegalArgumentException();
}
String sql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, request.getName(), request.getId());
}
@DeleteMapping("/user")
public void deleteUser(@RequestParam String name) {
String readSql = "SELECT * FROM user WHERE name = ?";
boolean isUserExist = jdbcTemplate.query(readSql (rs, rowNum) -> 0, name).isEmpty();
if (isUserNotExist) {
throw new IllegalArgumentException();
}
String sql = "DELETE FROM user WHERE name = ?";
jdbcTemplate.update(sql, name);
}
위와 같이 API에서 데이터 존재 여부를 확인해 예외를 던지면 됩니다.
String readSql = "SELECT * FROM user WHERE id = ?";
id를 기준으로 유저가 존재하는지 확인하기 위해 SELECT 쿼리를 작성했습니다.
boolean isUserExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0 request.getId()).isEmpty();
SQL을 날려 DB에 데이터가 있는지 확인합니다.
SELECT SQL의 결과가 있으면 0으로 변환됩니다.
최종적으로 List로 반환됩니다.
즉, 해당 id를 가진 유저가 있으면 0이 담긴 List가 나오고, 없다면 빈 List가 나오게 됩니다.
jdbcTemplate.query()의 결과인 List가 비어있다면, 유저가 없다는 뜻입니다.
if (isUserNotExist) {
throw new IllegalArgumentException();
}
만약 유저가 존재하지 않는다면 IllegalArgumentException을 던집니다.
-
☕️[Java] java에 대하여 간단하게 알아보기
☕️[Java] java에 대하여 간단하게 알아보기.
1️⃣ 각 코드 행의 의미.
int size = 27;
String name = "Fido";
Dog myDog = new Dog(name, size);
int x = size - 5;
if (x < 15) myDog.bark(8);
while (x > 3) {
myDog.play();
}
int[] numList = {2, 4, 6, 8};
System.out.println("Hello");
System.out.pring("Dog: " + name);
String num = "8";
int z = Integer.parseInt(num);
try {
readTheFile("myFile.txt");
}
catch (FileNoFoundException ex) {
System.out.print("File not found.")
}
‘size’라는 정수 변수를 선언하고 27이라는 값을 대입합니다.
‘name’라는 문자열 변수를 선언하고 Fido라는 값을 대입합니다.
‘myDog’라는 Dog 변수를 선언하고 ‘name’과 ‘size’를 사용해 새 Dog 객체를 만듭니다.
27(‘size’의 값)에서 5를 뺀 값을 ‘x’라는 변수에 대입합니다.
만약 x(값이 22)가 15보다 작으면 개가 여덟 번 짖도록 합니다.
x가 3보다 크면 반복문을 사용합니다.
개가 놀도록 지시합니다(play() 메서드를 실행시킴)
반복문이 끝나는 부분입니다. {} 안에 있는 것들이 조건에 따라 반복됩니다.
‘numList’라는 정수 배열을 선언하고 2,4,6,8을 집어넣습니다.
“Hello”를 출력합니다. (아마 명령행으로 출력될것입니다.)
명령행에 “Dog: Fido”를 출력합니다.
‘num’라는 문자열 변수를 선언하고 “8”이라는 값을 대입합니다.
“8”이라는 문자열을 8이라는 숫자값으로 변환합니다.
무언가를 시도해 봅니다. 잘 안 될 수도 있습니다.
“myFile.txe”를 읽습니다
“시도하는 부분”이 끝이므로 여러 가지를 함께 시도할 수도 있습니다.
시도했던 것이 실패하면 실행됩니다.
실패했을 경우에 명령행에 “File not found”라고 출력합니다.
중괄호({}) 안의 모든것은 ‘try’ 부분이 실패한 경우에 수행할 작업입니다.
2️⃣ 자바 코드의 구조
소스 파일 안에는 “클래스”가 들어갑니다.
클래스 안에는 “메서드”가 들어갑니다.
메서드 안에는 “명령문”이 들어갑니다.
소스 파일 안에는 무엇이 들어갈까요?
소스 코드 파일(.java라는 확장자가 붙은 파일)은 일반적으로 클래스(class) 를 한 개씩 정의합니다.
클래스는 보통 프로그램의 한 부분을 나타내지만, 아주 작은 애플리케이션 중에는 단 한 개의 클래스만으로 이뤄진 것도 있습니다.
클래스는 한 쌍의 중괄호({})로 둘러싸인 형태여야 합니다.
클래스 안에는 무엇이 들어갈까요?
하나의 클래스 안에는 하나 이상의 메서드(method) 가 들어갑니다.
예를 들어서,(개를 나타내는) Dog 클래스에는 (짖는 것을 의미하는) bark 라는 메서드가 들어갈 수 있으며, 이 메서드에는 개가 짖는 방법을 지시하는 내용이 들어갑니다.
메서드는 반드시 클래스 안에서 선언되어야 합니다.
즉, 클래스의 중괄호 안에 위치해야 합니다.
메서드 안에는 무엇이 들어갈까요?
메서드를 감싸는 중괄호 안에는 메서드에서 처리할 일을 지시하는 내용이 들어갑니다.
메서드 코드는 기본적으로 일련의 명령문을 모아 놓은 것이므로 지금은 메서드를 일종의 함수나 프로시저와 비슷한 것으로 생각하면 됩니다.
🙋♂️ 프로시저(Procedure)
-
-
🍃[Spring] `application.yml`과 `application.properties`의 차이점.
🍃[Spring] application.yml과 application.properties의 차이점.
Java 백엔드 애플리케이션에서 application.yml과 application.properties는 모두 애플리케이션의 설정 을 관리하는 데 사용되는 구성 파일입니다.
이 두 파일은 Spring Boot와 같은 프레임워크에서 애플리케이션의 환경 설정, 데이터베이스 연결, 포트 번호, 보안 설정 등을 정의하는 데 사용됩니다.
두 파일은 기능적으로 비슷하지만 형식과 가독성에서 차이가 있습니다.
1️⃣ 차이점.
1. 파일 형식.
application.properties
키-값 쌍 형식의 구성을 사용합니다.
각 설정은 한 줄에 하나씩, key=value 형식으로 작성됩니다.
application.yml
YAML 형식을 사용합니다.
YAML은 계층적 구조와 들여쓰기를 통해 설정을 정의하며, JSON과 유사한 방식으로 데이터를 구성합니다.
예시
application.properties 예시
server.port=8080
spring.dataspurce.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
logging.level.org.springframework=DEBUG
application.yml 예시
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
logging:
level:
org.springframework: DEBUG
2. 계층 구조 표현.
application.properties
각 설정 항목은 점 표기법(dot natation) 을 사용하여 계층 구조를 표현합니다.
예를 들어, spring.datasource.url 처럼 점을 사용해 중첩된 속성을 정의합니다.
application.yml
YAML은 들여쓰기를 통해 계층적 구조를 자연스럽게 표현할 수 있습니다.
점 표기법 대신 들여쓰기로 중첩된 구조를 표현합니다.
3. 가독성
application.properties
모든 설정이 한 줄에 키-값 쌍으로 표시되므로 간단한 설정에서는 읽기 쉽습니다.
그러나 계층적 데이터 구조를 표현해야 할 때는 점 표기법을 사용해야 하므로, 설정이 많아질 수록 읽기 어려워질 수 있습니다.
application.yml
YAML은 들여쓰기를 사용해 계층 구조를 표현하기 때문애, 복잡한 설정을 더 가독성 있게 표현할 수 있습니다.
설정이 많거나 중첩된 경우에도 더 명확하게 구성할 수 있습니다.
4. 데이터 표현의 유연성.
application.properties
단순히 키-값 쌍으로 데이터 표현이 제한됩니다.
배열이나 복잡한 데이터 구조를 표현할 때는 여러 줄에 걸쳐 점 표기법을 사용해야 합니다.
application.yml
YAML은 배열, 객체, 중첩된 구조를 쉽게 표현할 수 있습니다.
복잡한 데이터 구조를 표현하는 데 더 유연합니다.
배열 표현 예시.
application.properties 에서 배열을 표현하는 방법.
mylist[0]=item1
mylist[1]=item2
mylist[2]=item3
application.yml 에서 배열을 표현하는 방법.
```bash
mylist:
item1
item2
item3
```
5. 주석.
application.properties
주석은 # 기호로 시작합니다.
주석은 한 줄에 추가할 수 있습니다.
application.yml
주석도 # 기호를 사용합니다.
주석을 작성하는 방식은 application.properties와 동일하지만, YAML 형식에서는 여러 줄에 걸친 주석을 추가하기에 더 자연스럽습니다.
6. 사용 용도.
application.properties
단순한 설정을 정의할 때 유용합니다.
속성 수가 적고 계층적 구조가 많이 필요하지 않은 경우 더 직관적일 수 있습니다.
application.yml
복잡한 설정을 정의할 때 적합합니다.
YAML은 데이터의 계층적 구조를 쉽게 표현 할 수 있어, 중첩된 설정이나 다수의 설정이 필요한 경우 더 적합합니다.
2️⃣ 선택 기준
작은 프로젝트나 단순한 설정에는 application.properties가 적합할 수 있습니다.
점 표기법으로 간단히 설정할 수 있기 때문에 직관적이고 빠르게 설정을 적용할 수 있습니다.
복잡한 프로젝트나 다중적인 설정이 필요한 경우, 특히 설정 로깅 레벨 설정, 다중 환경 관리 등의 복잡한 구성이 요구될 때는 application.yml이 더 적합합니다.
YAML의 구조적 표현 덕분에 가독성과 유지보수성이 향상됩니다.
3️⃣ 요약.
application.properties
단순한 키-값 쌍으로 이루어진 설정 파일입니다.
점 표기법을 사용해 계층적 구조를 표현하며, 단순한 설정에 적합합니다.
application.yml
YAML 형식의 설정 파일로, 들여쓰기와 계층적 구조를 통해 복잡한 설정을 보다 직관적이고 가독성 있게 표현할 수 있습니다.
복잡한 프로젝트에서 유리합니다.
결국, 둘 다 동일한 기능을 수행할 수 있지만, 설정의 복잡도와 가독성 요구에 따라 properties와 yml 중 적합한 형식을 선택할 수 있습니다.
-
☕️[Java] jdbc란?
☕️[Java] jdbc란?
JDBC(Java Database Connectivity) 는 Java 애플리케이션에서 데이터베이스에 연결하고, 쿼리를 실행하며, 결과를 처리하기 위한 표준 API입니다.
JDBC는 Java에서 데이터베이스와 상호작용하는 방법을 정의하며, 데이터베이스와 애플리케이션 간의 통신을 가능하게 합니다.
이를 통해 개발자는 다양한 데이터베이스에서 일관된 방식으로 데이터에 접근할 수 있습니다.
1️⃣ JDBC의 주요 개념.
1. JDBC 드라이버(JDBC Driver)
JDBC는 드라이버 기반의 API입니다.
JDBC 드라이버는 특정 데이터베이스에 맞는 연결을 가능하게 하며, 데이터베이스에 대한 SQL 명령을 전달하고 결과를 받아 옵니다.
각 데이터베이스는 JDBC 표준을 따르는 드라이버를 제공해야 하며, MySQL, PostgreSQL, Oracle 등의 데이터베이스에는 각각의 JDBC 드라이버가 있습니다.
🙋♂️ 드라이버란?
2. JDBC API
JDBC는 데이터베이스와 통신하기 위한 다양한 인터페이스와 클래스를 포함하고 있습니다.
이를 통해 개발자는 데이터베이스 연결, SQL 쿼리 실행, 결과 처리, 트랜잭션 관리 등의 작업을 할 수 있습니다.
2️⃣ JDBC의 주요 작업 흐름
1. 데이터베이스 연결 설정(Establishing Connection)
애플리케이션은 JDBC 드라이버를 사용하여 데이터베이스에 연결을 설정합니다.
연결이 성공하면, 이를 통해 데이터베이스에 SQL 쿼리를 보낼 수 있습니다.
2. SQL 쿼리 실행(Executing SQL Queries)
연결이 설정되면, SQL 쿼리를 데이터베이스에 전달하여 데이터를 조회하거나 삽입, 업데이트, 삭제할 수 있습니다.
3. 결과 처리(Processing Results)
SQL 쿼리의 결과는 Java에서 처리할 수 있는 형식으로 반환됩니다.
조회 쿼리의 경우, ResultSet 객체로 결과가 반환되며, 이를 통해 데이터를 순차적으로 읽을 수 있습니다.
4. 트랜잭션 관리(Transaction Management)
JDBC는 트랜잭션 처리를 지원합니다.
데이터의 일관성을 보장하기 위해 트랜잭션을 커밋하거나 롤백할 수 있습니다.
5. 연결 해제(Closing Connection)
모든 작업이 끝나면, 데이터베이스 연결을 해제해야 합니다.
이를 통해 자원을 적절하게 반환하고 연결을 종료할 수 있습니다.
3️⃣ JDBC 코드 예시.
import java.sql.Connection;
import java.sql.DriverManagerl
import java.sql.ResultSet;
import java.sql.Statement;
public class JdbcExample {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet = resultSet = null;
try {
// 1. JDBC 드라이버 로드 (예: MySQL, JDBC 드라이버)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 데이터베이스 연결 설정
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");
// 3. SQL 쿼리 실행
statement = connection.createStatement();
resultSet = statement.executeQuery("SELECT * FROM Users");
// 4. 결과 처리
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String email = resultSet.getString("email");
System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 리소스 해제
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4️⃣ 주요 JDBC 객체
1. Connection
데이터베이스와 연결을 나타내는 객체입니다.
이 객체는 연결을 생성, 유지, 종료하는 데 사용되며, SQL 쿼리 실행에 필요한 통로 역할을 합니다.
연결은 DriverManager.getConnection() 메서드를 통해 생성됩니다.
2. Statement
SQL 쿼리를 데이터베이스로 보내기 위한 객체입니다.
Statement는 정적 SQL 쿼리를 실행하는 데 사용되며, 쿼리 결과를 받아 처리할 수 있습니다.
3. PreparedStatement
Statement의 확장된 버전으로, 미리 컴파일된 SQL 쿼리를 실행할 수 있습니다.
이는 동적 데이터가 포함된 쿼리(파라미터화된 쿼리)를 처리할 때 사용되며, 보안성과 성능을 개선할 수 있습니다.
예시
PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM Users WHERE id = ?");
pstmt.setInt(1, 1); // 1번째 매개변수에 1을 설정
ResultSet rs = pstmt.executeQuery();
4. ResultSet
SQL 쿼리 실행 결과를 담는 객체입니다.
ResultSet은 데이터베이스로부터 반환된 결과를 테이블 형식으로 제공하며, 이를 통해 결과를 순차적으로 읽고 처리할 수 있습니다.
5. DriverManager
JDBC 드라이버를 관리하는 클래스입니다.
데이터베이스 연결을 생성할 때 DriverManager.getConnection() 메서드를 사용하여 적절한 JDBC 드라이버를 통해 연결을 설정합니다.
5️⃣ JDBC의 장점.
1. 플랫폼 독립성.
JDBC는 Java 표준 API로, 데이터베이스 시스템과 독립적으로 동작합니다.
다양한 데이터베이스 시스템에 대한 통일된 방식으로 접근할 수 있습니다.
2. 표준화된 API.
JDBC는 Java 표준 API로, 데이터베이스 시스템과 독립적으로 동작합니다.
다양한 데이터베이스 시스템에 대한 통일된 방식으로 접근할 수 있습니다.
3. 광범위한 데이터베이스 지원.
JDBC는 MySQL, PostgreSQL, Oracle, SQL Server 등 다양한 데이터베이스 시스템을 지원합니다.
각각의 데이터베이스는 JDBC 드라이버를 통해 연결됩니다.
4. 트랜잭션 지원.
JDBC는 데이터베이스 트랜잭션을 관리할 수 있으며, 자동 커밋 또는 수동 트랜잭션 관리를 통해 데이터 일관성을 보장합니다.
6️⃣ JDBC의 단점.
1. 저수준 API.
JDBC는 SQL 쿼리 실행과 같은 저수준 작업을 다루기 때문에 코드가 길어지고 복잡해줄 수 있습니다.
많은 설정 코드와 예외 처리가 필요합니다.
2. SQL 종속성.
JDBC는 SQL 쿼리를 직접 사용하기 때문에, 데이터베이스마다 SQL 문법의 차이로 인해 호환성 문제가 발생할 수 있습니다.
즉, 데이터베이스 간에 이식성이 완벽하지 않을 수 있습니다.
3. ORM의 등장.
Hibernate, JPA와 같은 ORM(Object-Relational Mapping) 프레임워크는 JDBC보다 더 높은 수준의 추상화를 제공하여, 데이터베이스와의 상호작용을 더 쉽게 관리할 수 있습니다.
아로 인해 복잡한 JDBC 코드를 간소화할 수 있습니다.
7️⃣ 요약
JDBC는 Java 애플리케이션에서 데이터베이스에 연결하고, SQL 쿼리를 실행하며, 결과를 처리하는 표준 API입니다.
JDBC는 다양한 데이터베이스 시스템과 상호작용할 수 있는 일관된 인터페이스를 제공하며, 연결 설정, SQL 쿼리 실행, 트랜잭션 관리, 결과 처리와 같은 작업을 수행합니다.
JDBC는 저수준 API로서 강력한 기능을 제공하지만, ORM 프레임워크가 복잡한 데이터베이스 작업을 보다 쉽게 처리할 수 있게 함으로써 최근에는 JDBC를 기반으로 더 고수준의 라이브러리가 자주 사용되고 있습니다.
🙋♂️ 라이브러리와 프레임워크의 차이점
-
💾 [CS] yml이란?
💾 [CS] yml이란?
YML(또는 YAML, YAML Ani’t Markup Language) 은 사람이 읽기 쉬운 데이터 직렬화 형식입니다.
주로 애플리케이션 구성(configuration) 파일을 정의하는 데 사용됩니다.
YAML은 데이터 구조를 표현하기 위한 간단하고 가독성 높은 포맷으로, Python의 들여쓰기 방식과 유사한 계층적 구조를 사용합니다.
JSON과 유사한 기능을 제공하면서도 더 간결하고 인간이 읽고 쓰기 쉬운 문법을 사용합니다.
1️⃣ YAML의 주요 특징.
1. 간결한 문법.
YAML은 간결한 문법을 사용하여 데이터를 표현합니다.
구분 기호로 주로 콜론(:), 하이픈(-) 등 을 사용하고, 들여쓰기를 통해 계층 구조를 나타냅니다.
이러한 간단한 구조는 사람과 기계 모두 쉽게 읽을 수 있도록 도와줍니다.
2. 계층적 데이터 구조.
YAML은 들여쓰기를 통해 계층적 데이터 구조를 표현합니다.
이는 키-값 쌍, 목록, 중첩된 데이터 구조를 직관적으로 표현하는 데 유리합니다.
3. 다양한 데이터 형식 지원.
YAML은 문자열, 숫자, 배열, 객체 등 다양한 데이터 타입을 지원합니다.
이를 통해 복잡한 데이터 구조를 직렬화하고 전송할 수 있습니다.
4. 주로 설정 파일로 사용.
YAML은 구성 파일을 정의하는 데 자주 사용됩니다.
예를 들어, Docker, Ansible, Kubernetes, Spring Boot와 같은 도구들을 설정을 정의하기 위해 YAML을 사용합니다.
5. 주석 지원.
YAML은 주석을 지원하며, 주석은 # 기호로 시작합니다.
주석은 설명을 추가하여 사람이 더 쉽게 이해할 수 있도록 도와줍니다.
2️⃣ YAML 문법 예시
1. 키-값 쌍(Key-Value Pairs)
name: Kobe
age: 30
email: kobe@example.com
이 예시는 키-값 쌍을 정의하는 YAML의 기본 형태입니다.
각 키는 콜론(:) 으로 값과 구분되며, 간결하게 표현됩니다.
2. 리스트(List)
fruits:
- Apple
- Banan
- Orange
하이픈(-) 을 사용하여 리스트 항목을 나타냅니다.
이 리스트는 fruits라는 키에 연결된 배열입니다.
3. 중첩된 데이터 구조(Nested Data)
person:
name: Kobe
age: 25
contact:
email: kobe@example.com
phone: 123-4567-8910
YAML에서는 들여쓰기를 사용하여 계층 구조를 나타냅니다.
contact는 person 객체 안에 포함된 중첩된 객체입니다.
4. 리스트와 키-값 쌍의 혼합
employees:
- name: Minseong
age: 30
position: Developer
- name: Naeun
age: 25
position: Designer
이 예시에서는 리스트 안에 여러 객체가 포함된 구조를 나타냅니다.
각 객체는 name, age, position과 같은 키-값 쌍으로 구성됩니다.
5. 주석.
# 이 파일은 서버 구성 파일입니다.
server:
host: localhost
port: 8080 # 기본 포트는 8080입니다.
# 을 사용해 주석을 추가할 수 있으며, 주석은 파일에 설명을 추가하는 데 사용됩니다.
3️⃣ YAML의 주요 사용 사례
1. 구성 파일(Configuration Files)
YAML은 주로 구성 파일로 사용됩니다.
여러 애플리케이션에서 설정을 정의하고, 구조적 데이터를 간단하게 표현하는 데 적합합니다.
예를 들어, Spring Boot의 application.yml 파일에서는 데이터베이스 설정이나 서버 설정을 관리할 수 있습니다.
2. DevOps 도구
YAML은 Ansible, Docker Compose, Kubernetes와 같은 DevOps 도구의 설정 파일 형식으로 널리 사용됩니다.
YMAL을 사용하여 서버 설정, 컨테이너 설정, 애플리케이션 배포 전략 등을 정의할 수 있습니다.
예시: Docker Compos 설정 파일
version: '3'
services:
web:
image: nginx
ports:
- "8080:80"
db:
image: postgres
enviroment:
POSTGES_USER: user
POSTGRES_PASSWORD: password
4️⃣ YAML의 장점.
1. 간결하고 읽기 쉬움.
YAML은 사람이 읽고 쓰기 쉽게 설계된 데이터 형식입니다.
중첩된 데이터 구조를 직관적으로 표현할 수 있어, JSON보다 더 간결하게 데이터를 나타낼 수 있습니다.
2. 다양한 응용 프로그램에서 사용.
YAML은 다양한 시스템과 도구에서 구성 파일로 널리 사용되며, 데이터 직렬화 포맷으로 활용됩니다.
3. 구조적 데이터 표현.
YAML은 복잡한 데이터 구조를 직관적이고 명확하게 표현할 수 있어, 중첩된 객체나 배열을 다루기 적합합니다.
4. 주석 지원.
주석을 통해 구성 파일에 설명을 추가할 수 있어 다른 사람들이 더 쉽게 이해할 수 있습니다.
5️⃣ YAML의 단점.
1. 곰백에 민감.
YAML은 들여쓰기가 중요한 역할을 하기 때문에, 잘못된 들여쓰기나 공백으로 인해 파싱 오류가 발생할 수 있습니다.
정확한 공백과 들여쓰기를 유지하는 것이 중요합니다.
2. 대용량 데이터 처리 비효율성.
YAML은 주로 구성 파일과 같은 간단한 데이터 구조를 표현하는 데 적합하며, 대용량 데이터를 직렬화할 때는 JSON이나 다른 포맷보다 효율성이 떨어질 수 있습니다.
7️⃣ 요약.
YAML(YML) 은 간결하고 사람이 읽기 쉬운 데이터 직렬화 포맷으로, 주로 구성 파일을 정의하는 데 사용됩니다.
들여쓰기를 사용하여 계층적 데이터를 표현하며, 여러 DevOps 도구와 애플리케이션의 설정 파일에서 널리 활용됩니다.
YAML은 읽기 쉽고 주석을 지원하는 장점이 있지만, 들여쓰기에 민감하기 때문에 구문 오류를 유발할 수 있습니다.
YAML은 단순하고 복잡한 데이터 구조를 직관적으로 표현하는 데 적합한 포맷입니다.
-
💾 [CS] 드라이버(Driver)란?
💾 [CS] 드라이버(Driver)란?
드라이버(Driver) 는 컴퓨터에서 하드웨어 장치나 소프트웨어와 운영체제 또는 다른 소프트웨어 간의 상호작용을 가능하게 하는 소프트웨어입니다.
드라이버는 하드웨어나 소프트웨어의 구체적인 작동 방식과 기능을 운영체제 또는 애플리케이션이 이해할 수 있도록 중개하는 역할을 합니다.
1️⃣ 드라이버의 주요 개념.
1. 하드웨어 드라이버(Hardware Driver)
하드웨어 드라이버는 컴퓨터의 운영체제와 하드웨어 장치 간의 소통을 가능하게 합니다.
하드웨어 드라이버는 운영체제가 하드웨어 장치(프린터, 그래픽 카드, 네트워크 카드 등)의 세부적인 기능을 직접 알지 못해도, 하드웨어를 사용할 수 있도록 필요한 명령을 전달하는 역할을 합니다.
예시
프린터 드라이버
프린터 드라이버는 운영체제와 프린터 간의 중간 역할을 하여, 운영체제가 프린터와 소통하고 문서를 출력할 수 있게 합니다.
그래픽 카드 드라이버
그래픽 카드 드라이버는 운영체제가 그래픽 카드를 제어하여 모니터에 이미지를 출력할 수 있도록 합니다.
2. 소프트웨어 드라이버(Software Driver)
소프트웨어 드라이버는 특정 소프트웨어와 운영체제 간의 상호작용을 돕습니다.
소프트웨어 드라이버는 보통 소프트웨어와 운영체제 간의 특정 작업을 수행하거나, 소프트웨어가 하드웨어 자원에 접근할 수 있도록 지원하는 역할을 합니다.
예시
JDBC 드라이버
Java 애플리케이션이 데이터베이스와 상호작용할 수 있도록 지원하는 소프트웨어 드라이버입니다.
데이터베이스별로 JDBC 드라이버가 제공되며, Java 애플리케이션은 이 드라이버를 통해 데이터베이스에 SQL 쿼리를 보내고 결과를 받아올 수 있습니다.
3. 가상 드라이버(Virtual Driver)
물리적인 하드웨어 장치와는 달리, 가상 드라이버는 가상화된 하드웨어 또는 시스템 리소스와 상호작용할 수 있도록 지원합니다.
이는 가상 머신이나 에뮬레이터에서 주로 사용됩니다.
예시
가상 네트워크 드라이버
가상 머신 내에서 네트워크 연결을 지원하기 위해 가상 드라이버가 사용될 수 있습니다.
이는 실제 네트워크 인터페이스 카드(NIC)를 사용하지 않더라도 가상 환경에서 네트워크 통신이 가능하도록 해줍니다.
2️⃣ 드라이버의 역할
1. 운영체제와 하드웨어 간의 인터페이스 제공.
드라이버는 하드웨어 장치의 저수준의 명령과 운영체제에서 사용하는 고수준의 명령 간에 변환을 제공합니다.
예를 들어, 운영체제는 드라이버를 통해 하드웨어에 명령을 전달하고, 하드웨어는 그에 대한 응답을 운영체제에 보냅니다.
2. 추상화.
운영체제는 하드웨어의 세부 구현 방식을 알지 않고도 드라이버를 통해 하드웨어와 상호작용할 수 있습니다.
드라이버는 하드웨어의 구체적인 동작을 숨기고, 운영체제에 표준화된 인터페이스를 제공합니다.
3. 하드웨어 제어.
드라이버는 하드웨어의 특정 기능을 제어하는 데 중요한 역할을 합니다.
예를 들어, 그래픽 드라이버는 화면 해상도, 색상, 디스플레이 모드 등을 제어할 수 있습니다.
4. 데이터 전달.
드라이버는 운영체제와 하드웨어 간의 데이터 전송을 담당합니다.
네트워크 드라이버는 데이터를 네트워크를 통해 송수신하는 역할을 하고, 스토리지 드라이버는 파일 시스템의 데이터를 하드 디스크로 읽고 쓰는 작업을 처리합니다.
3️⃣ 드라이버의 종류.
1. 장치 드라이버(Device Driver)
컴퓨터의 하드웨어 장치(프린터, 마우스, 키보드, 그래픽 카드 등)를 제어하기 위해 사용하는 드라이버입니다.
예시: 그래픽 카드 드라이버, 사운드 카드 드라이버, 프린터 드라이버
2. 파일 시스템 드라이버(File System Driver)
특정 파일 시스템(FAT, NTFS, ext4 등)에서 데이터를 읽고 쓸 수 있도록 지원하는 드라이버입니다.
예시: NTFS 드라이버, ext4 드라이버
3. 네트워크 드라이버(Network Driver)
네트워크 인터페이스 카드(NIC)를 제어하여 컴퓨터가 네트워크에 연결되고 데이터를 주고받을 수 있도록 하는 드라이버입니다.
예시: 이더넷 카드 드라이버, Wi-Fi 드라이버
4. 가상 드라이버(Virtual Driver)
가상 하드웨어나 가상 시스템을 지원하기 위해 사용되는 드라이버로, 물리적 장치가 없는 환경에서도 동작할 수 있도록 합니다.
예시: 가상 네트워크 어댑터 드라이버
5. 데이터베이스 드라이버(Database Driver)
소프트웨어 드라이버의 일종으로, 애플리케이션이 데이터베이스와 상호작용할 수 있도록 하는 소프트웨어입니다.
JDBC 드라이버가 여기에 해당됩니다.
예시: MySQL, JDBC 드라이버, Oracle JDBC 드라이버
4️⃣ 드라이버의 동작 원리.
1. 하드웨어 장치와 통신
드라이버는 하드웨어 장치에 대한 명령어를 전달하고, 장치로부터 데이터를 수신합니다.
하드웨어가 데이터를 요청하면, 드라이버는 그 요청을 운영체제에 전달하고, 그 반대도 수행합니다.
2. 인터럽트 처리
하드웨어 장치가 특정 작업을 완료했을 때, 드라이버는 운영체제에 인터럽트를 통해 알립니다.
운영체제는 이 인터럽트를 받아 하드웨어의 작업 완료 상태를 확인하고, 필요한 후속 작업을 수행합니다.
3. I/O 요청 처리
드라이버는 운영체제의 입출력(I/O) 요청을 처리하는 데 중요한 역할을 합니다.
예를 들어, 사용자가 파일을 저장하거나 네트워크로 데이터를 전송하려고 하면, 드라이버는 이 요청을 하드웨어로 전달하여 해당 작업을 수행합니다.
5️⃣ 드라이버의 예시.
1. JDBC 드라이버.
JDBC 드라이버는 데이터베이스와 Java 애플리케이션 간의 상호작용을 가능하게 하는 소프트웨어 드라이버입니다.
애플리케이션이 데이터베이스와 통신할 때 SQL 쿼리를 전송하고 결과를 처리할 수 있도록 지원합니다.
2. 그래픽 카드 드라이버.
그래픽 카드 드라이버는 운영체제가 그래필 카드를 제어하고, 화면에 이미지를 출력할 수 있도록 돕습니다.
게임, 그래픽 소프트웨어, 비디오 편집 소프트웨어 등에서 그래픽 카드의 성능을 최적화할 수 있습니다.
3. 프린터 드라이버.
프린터 드라이버는 운영체제가 프린터로 문서를 전송하여 인쇄할 수 있도록 해줍니다.
드라이버는 운영체제가 프린터의 특정 기능을 이해할 수 있도록 중개 역할을 합니다.
6️⃣ 요약.
드라이버는 소프트웨어와 하드웨어 또는 다른 소프트웨어 간의 상호작용을 가능하게 하는 중간 소프트웨어입니다.
하드웨어 드라이버는 운영체제가 하드웨어 장치와 통신하도록 돕고, 소프트웨어 드라이버는 소프트웨어가 다른 시스템 리소스나 서비스와 상호작용할 수 있도록 지원합니다.
드라이버는 시스템 자원을 효율적으로 사용할 수 있도록 하고, 운영체제와 하드웨어/소프트웨어 간의 추상화를 제공하여 표준화된 인터페이스를 제공합니다.
-
💉[SQL] DML이란?
💉[SQL] DML이란?
DML(Data Manipulation Language) 은 데이터베이스에서 데이터를 조작하는데 사용되는 SQL의 하위 언어입니다.
DML 명령어는 데이터베이스 테이블에 데이터를 삽입, 수정, 삭제, 조회하는 작업을 수행합니다.
DML은 주로 테이블 내의 데이터를 다루는 데 사용되며, 데이터베이스의 구조(예: 테이블 생성 또는 삭제)를 변경하지 않습니다.
1️⃣ DML의 주요 명령어
1. SELECT
데이터베이스에서 데이터를 조회하는 명령어입니다.
테이블에 저장된 데이터를 선택적으로 가져오거나 특정 조건에 맞는 데이터를 검색할 수 있습니다.
예시: 데이터 조회
SELECT * FROM User WHERE age > 25;
Users 테이블에서 age가 25보다 큰 모든 행을 조회합니다.
2. INSERT
테이블에 데이터를 삽입하는 명령어입니다.
테이블의 각 열(Column)에 맞는 데이터를 새 행(Row)으로 추가합니다.
예시: 데이터 삽입
INSERT INTO Users (name, email, age) VALUES ('Kobe', 'kobe@example.com', 30);
Users 테이블에 새로운 사용자의 데이터를 삽입합니다.
3. UPDATE
테이블의 기존 데이터를 수정하는 명령어입니다.
특정 조건을 만족하는 데이터를 찾아 수정할 수 있습니다.
예시: 데이터 수정
UPDATE Users SET age = 31 WHRER name = 'Kobe';
Users 테이블에서 이름이 'Kobe'인 사용자의 나이를 31로 수정합니다.
4. DELETE
테이블에서 데이터를 삭제하는 명령어입니다.
특정 조건을 만족하는 데이터를 삭제하거나, 모든 데이터를 삭제할 수 있습니다.
예시: 데이터 삭제
DELETE FROM Users WHERE age < 18;
Users 테이블에서 나이가 18 미만인 모든 행(Row)을 삭제합니다.
2️⃣ DML의 특징.
1. 데이터 조작.
DML은 데이터베이스 내에서 데이터를 직접적으로 조작하는 데 사용됩니다.
데이터를 추가하거나 삭제하고, 기존 데이터를 조회하거나 수정하는 등의 작업을 수행합니다.
2. 트랜잭션과 연관.
DML 명령어는 트랜잭션 관리와 밀접한 관련이 있습니다.
DML 명령어로 데이터를 변경하는 작업은 트랜잭션 내에서 수행될 수 있으며, 트랜잭션의 커밋(COMMIT) 또는 롤백(ROLLBACK) 을 통해 데이터의 일관성과 무결성을 보장할 수 있습니다.
3. 트랜잭션 필요.
DML 작업은 데이터베이스에서 실제 데이터를 변경하는 작업이므로 트랜잭션이 필요합니다.
데이터 변경 작업이 완료되면 COMMIT을 통해 저장하고, 오류가 발생하면 ROLLBACK을 통해 변경 작업을 취소할 수 있습니다.
4. 데이터 구조 변경 없음
DML은 데이터를 다루는 데 초점을 맞추며, 테이블이나 데이터베이스 구조 자체를 변경하지는 않습니다.
구조 변경은 DDL(Data Definition Language)의 역할입니다.
3️⃣ DML 사용 예시
1. 데이터 조회(SELECT)
SELECT name, email FROM Users WHERE age > 25;
Users 테이블에서 나이가 25 이상인 사용자들의 이름과 이메일을 조회합니다.
2. 데이터 삽입(INSERT)
INSERT INTO Users (name, email, age) VALUES ('Kobe', 'kobe@example.com', 28);
Users 테이블에 새로운 사용자인 Kobe를 추가합니다.
3. 데이터 수정(UPDATE)
UPDATE Users SET email = 'dev.kobe@example.com' WHERE name = 'Kobe';
Users 테이블에서 이름이 Kobe인 사용자의 이메일을 수정합니다.
4. 데이터 삭제(DELETE)
DELETE FROM Users WHERE id = 3;
Users 테이블에서 id가 3인 사용자의 데이터를 삭제합니다.
4️⃣ DML과 DDL의 차이점.
DML(Data Manipulation Language)
DML은 데이터 자체를 조작하는 데 사용됩니다.
SELECT, INSERT, UPDATE, DELETE와 같은 명령어를 통해 데이터를 삽입, 조회, 수정, 삭제할 수 있습니다.
트랜잭션 처리(커밋, 롤백)가 필요하며, 데이터베이스 내에서 데이터 조작이 중심입니다.
DDL(Data Definition Language)
DDL은 데이터베이스의 구조를 정의하는 데 사용됩니다.
CREATE, ALTER, DROP 등의 명령어를 통해 테이블과 같은 데이터베이스 객체를 생성, 수정, 삭제할 수 있습니다.
DDL은 일반적으로 트랜잭션 관리 없이 자동 커밋되며, 데이터베이스의 구조 관리가 중심입니다.
5️⃣ 요약.
DML(Data Manipulation Language)은 데이터베이스에서 데이터를 삽입, 조회, 수정, 삭제하는 작업을 수행하는 SQL 언어입니다.
DML은 데이터 자체를 직접적으로 조작하는 데 사용되며, 트랜잭션과 연관이 깊어 작업의 무결성을 보장하기 위해 COMMIT이나 ROLLBACK을 통해 작업의 완료 여부를 확인할 수 있습니다.
DML은 데이터를 효율적으로 다루고 관리하는 데 필수적인 역할을 하며, 실제 데이터 조작에 집중하는 언어입니다.
-
💉[SQL] DDL이란?
💉[SQL] DDL이란?
DDL(Data Definition Language) 은 데이터베이스에서 데이터 구조를 정의하고 관리하는 데 사용되는 SQL의 하위 언어 입니다.
DDL 명령어는 데이터베이스의 테이블, 인덳, 스키마, 뷰 등과 같은 객체들을 생성, 수정, 삭제하는 작업을 수행합니다.
데이터베이스 내에서 데이터를 저장할 구조를 설정하고, 그 구조를 변경하거나 삭제하는 역할을 합니다.
1️⃣ DDL의 주요 명령어.
1. CREATE
데이터베이스 객체(테이블, 인덱스, 스키마, 뷰 등) 를 생성합니다.
예를 들어, 테이블을 생성할 때 CREATE TABLE 명령어를 사용하여 테이블의 구조를 정의합니다.
예시: 테이블 생성.
CREATE TABLE Users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
2. ALTER
기존에 생성된 데이터베이스 객체의 구조를 수정하는데 사용됩니다.
테이블에 새로운 열을 추가하거나, 열의 데이터 타입을 변경하는 등의 작업을 수행할 수 있습니다.
예시: 테이블 열 추가
ALTER TABLE Users
ADD COLUMN age INT;
3. DROP
데이터베이스 객체를 삭제하는 데 사용됩니다.
테이블, 인덱스, 뷰 등의 객체를 삭제할 때 사용합니다.
DROP 명령어는 매우 강력하며, 한번 객체가 삭제되면 그 안의 모든 데이터도 함께 사라지기 때문에 주의가 필요합니다.
예시: 테이블 삭제
DROP TABLE Users;
4. TRUNCATE
테이블의 모든 데이터를 삭제 하지만, 테이블의 구조는 유지합니다.
데이터만 삭제되고, 테이블 자체는 남아 있으므로 테이블을 다시 사용할 수 있습니다.
DELETE 명령어와는 달리, 개별 트랜잭션을 기록하지 않고 데이터를 일괄적으로 삭제하므로 더 빠르게 수행됩니다.
예시: 테이블 데이터 모두 삭제.
TRUNCATE TABLE Users;
2️⃣ DDL의 주요 특징.
데이터 구조 정의.
DDL 명령어는 데이터를 저장할 테이블과 같은 구조를 정의하는 역할을 합니다.
테이블 생성, 수정, 삭제 같은 작업이 여기에 포함됩니다.
자동 커밋.
DDL 명령어는 실행 즉시 자동으로 커밋(Commit) 됩니다.
즉, CREATE, ALTER, DROP 같은 명령어는 트랜잭션으로 묶이지 않고, 실행과 동시에 즉시 데이터베이스에 반영됩니다.
비교적 단순한 작업.
DDL 명령어는 데이터를 직접 조작하지 않고, 데이터를 저장할 구조를 다루는 데 초점을 맞춥니다.
3️⃣ DDL과 DML의 차이점.
DDL(Data Definition Language)
데이터베이스의 구조를 정의하는 언어입니다.
주로 테이블 인덱스 등의 데이터 구조를 생성, 변경, 삭제하는 데 사용됩니다.
DML(Data Manipulation Language)
데이터베이스 내의 데이터를 조작하는 언어입니다.
데이터를 삽입(INSERT), 조회(SELECT), 수정(UPDATE), 삭제(DELETE)하는 작업이 DML에 해당합니다.
4️⃣ DDL 사용 예시.
-- 테이블 생성
CREATE TABLE Employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(100),
emp_salary DECIMAL(10, 2)
);
-- 테이블 수정 (새로운 열 추가)
ALTER TABLE Employees
ADD COLUMN emp_department VARCHAR(50);
-- 테이블 데이터 일괄 삭제 (테이블 구조는 유지)
TRUNCATE TABLE Employees;
-- 테이블 삭제
DROP TABLE Employees;
5️⃣ 요약.
DDL(Data Definition Language)은 데이터베이스에서 테이블, 인덱스, 스키마와 같은 데이터 구조를 생성하고 관리하는 데 사용되는 SQL 언어입니다.
DDL은 데이터의 저장 구조를 정의하며, 주요 명령어로는 CREATE, ALTER, DROP, TRUNCATE가 있습니다.
DDL 명령어는 데이터베이스의 구조를 변경할 때 사용되며, 데이터 자체를 다루지 않고, 테이블과 같은 데이터 저장소의 형식을 정의하는 데 중점을 둡니다.
-
💾[Database] 데이터베이스에서 테이블을 만든다는 의미는 무엇일까?
💾[Database] 데이터베이스에서 테이블을 만든다는 의미는 무엇일까?
데이터베이스에서 테이블을 만든다는 것은, 데이터를 저장할 구조를 정의하는 것을 의미합니다.
테이블은 데이터베이스 내에서 정보를 저장하는 기본 단위로, 행(Row) 과 열(Column) 로 구성된 표(Table) 형태로 관리합니다.
1️⃣ 테이블 생성의 의미.
1. 데이터 구조의 정의.
테이블을 만든다는 것은 특정 유형의 데이터를 저장할 수 있는 데이터 구조를 정의하는 것입니다.
각 열(Column)은 저장될 데이터의 속성을 나타내며, 각 행(Row)은 데이터의 개별 항목(레코드) 나타냅니다.
예를 들어, Users 테이블을 만든다면 사용자에 대한 정보를 저장할 수 있도록, 이름, 이메일, 나이와 같은 속성을 정의하는 것입니다.
2. 데이터 타입 설정.
테이블의 각 열(Column)은 특정 데이터 타입을 가집니다.
데이터 타입은 해당 열(Column)에 어떤 종류의 데이터가 저장될 수 있는지를 결정합니다.
예를 들어, name 열(Column)은 문자열 데이터를 저장하고 age 열(Column)은 숫자 데이터를 저장할 수 있습니다.
3. 제약 조건 설정.
테이블 생성 시, 제약 조건을 설정하여 데이터의 무결성을 보장할 수 있습니다.
예를 들어, 기본 키(Primary Key)를 설정하여 각 레코드를 고유하게 식별하거나, 특정 열(Column)에 NOT NULL 제약을 걸어 해당 열(Column)이 비어 있을 수 없도록 설정할 수 있습니다.
2️⃣ 예시: 테이블 생성.
CREATE TABLE Users (
id INT KEY, -- Primary Key: 각 사용자를 고유하게 식별
name VARCHAR(100) NOT NULL, -- 이름: 문자열, null 값을 허용하지 않음
email VARCHAR(100) UNIQUE, -- 이메일: 고유한 문자열
age INT, -- 나이: 정수
created_at TIMESTAMP -- 생성 시간: 타임스탬프
);
위의 SQL 예시는 Users라는 테이블을 정의하는 것입니다.
테이블이 정의되면 해당 구조에 맞게 데이터를 저장할 수 있습니다.
id 열은 정수형 데이터 타입으로, 각 사용자를 고유하게 식별하는 기본 키(Primary Key)입니다.
name 열은 VARCHAR(100) 데이터 타입으로 최대 100자의 문자열을 저장하며, null 값을 허용하지 않습니다.
email 열은 UNIQUE 제약을 가집니다. 즉, 테이블 내에서 중복된 이메일이 저장될 수 없습니다.
age 열은 정수형 데이터를 저장할 수 있습니다.
created_at 열은 타임스탬프로, 데이터가 생성된 시간을 기록하는 열입니다.
3️⃣ 테이블 생성의 목적
1. 데이터 저장 및 관리.
테이블을 생성하면, 해당 구조에 맞게 데이터를 저장할 수 있습니다.
각 행(Row)은 하나의 레코드를 나타내며, 테이블에 정의된 각 열(Column)은 그 레코드의 속성을 나타냅니다.
2. 데이터 무결성 보장.
테이블을 생성할 때 정의하는 제약 조건(Primary Key, Foreign Key, Unique, Not Null 등)은 데이터의 정확성과 일관성을 보장합니다.
3. 효율적인 데이터 조회.
테이블을 통해 데이터를 구조화하여 저장하면, SQL 쿼리를 통해 효율적으로 데이터를 검색, 수정, 삭제할 수 있습니다.
적절한 인덱스와 관계 설정을 통해 데이터베이스 성능을 최적화할 수 있습니다.
4️⃣ 테이블을 만든 후
테이블을 만든 후, 해당 테이블에 데이터를 삽입하고, 필요에 따라 데이터를 조회, 수정, 삭제할 수 있습니다.
테이블은 데이터베이스에서 데이터를 논리적으로 그룹화하고 관리하는 핵심적인 역할을 합니다.
-
💾 [CS] SQL이란?
💾 [CS] SQL이란?
SQL(Structured Query Language) 는 관계형 데이터베이스에서 데이터를 관리하고 조작하기 위한 표준 언어입니다.
SQL은 데이터를 검색, 삽입, 수정 삭제하는 작업을 지원하며, 데이터베이스에서 저장된 데이터를 효율적으로 관리할 수 있도록 다양한 명령어를 제공합니다.
관계형 데이터베이스 관리 시스템(RDBMS, Relational Database Management System)에서 데이터를 처리하는 데 사용되며, 데이터베이스 테이블 내의 데이터를 다루기 위한 언어입니다.
1️⃣ SQL의 주요 특징.
1. 관계형 데이터베이스 관리.
SQL은 관계형 데이터베이스에서 테이블 형태로 데이터를 관리합니다.
테이블은 행(row), 열(column)로 구성되며, 각 테이블은 고유한 데이터를 저장합니다.
2. 표준 언어.
SQL은 국제 표준화 기구(ISO) 와 미국 표준협회(ANSI) 에서 정의한 표준 언어로, 여러 관계형 데이터베이스 시스템(예: MySQL, PostgreSQL, Oracle, Microsoft SQL Server)에서 사용됩니다.
3. 데이터베이스 조작 및 관리.
SQL을 사용하여 테이블에 데이터를 삽입, 수정, 삭제할 수 있으며, 데이터를 검색하거나 데이터 구조를 정의(테이블 생성, 변경)할 수도 있습니다.
4. 데이터 쿼리
SQL은 데이터를 질의(Query, 쿼리) 하기 위한 언어입니다.
사용자는 SQL 쿼리를 통해 데이터베이스에서 특정 조건에 맞는 데이터를 검색하거나, 집계할 수 있습니다.
2️⃣ SQL의 주요 기능 및 분류.
SQL은 크게 네 가지 유형의 명령어로 분류됩니다.
1. 데이터 정의 언어(DDL, Data Definition Language)
데이터베이스의 구조를 정의하는 데 사용됩니다.
테이블, 인덱스, 스키마 등의 생성과 삭제를 포함합니다.
주요 명령어
CREATE: 데이터베이스 객체(테이블, 인덱스 등)를 생성합니다.
ALTER: 데이터베이스 객체를 수정합니다.
DROP: 데이터베이스 객체를 삭제합니다.
예시.
CREATE TABLE Users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
2. 데이터 조작 언어(DML, Data Manipulation Language)
데이터베이스의 데이터를 삽입, 수정, 삭제하는 데 사용됩니다.
주요 명령어
INSERT: 데이터를 테이블에 삽입합니다.
UPDATE: 테이블에 있는 데이터를 수정합니다.
DELETE: 테이블에서 데이터를 삭제합니다.
예시.
INSERT INTO Users (id, name, email) VALUES (1, 'Kobe', 'kobe@example.com');
3. 데이터 쿼리 언어(DQL, Data Query Language)
데이터를 검색하거나 조회하는 데 사용됩니다.
가장 많이 사용되는 명령어는 SELECT입니다.
주요 명령어
SELECT: 테이블에서 데이터를 검색합니다. 다양한 조건을 통해 특정 데이터만을 선택적으로 조회할 수 있습니다.
예시
SELECT * FROM Users WHERE name = 'Kobe';
4. 데이터 제어 언어(DCL, Data Control Language)
데이터베이스에서 사용자 권한을 관리하고 제어하는 데 사용됩니다.
주요 명령어
GRANT: 사용자에게 권한을 부여합니다.
REVOKE: 사용자로부터 권한을 회수합니다.
예시
GRANT SELECT ON Users TO some_user;
5. 트랜잭션 제어 언어(TCL, Transaction Control Language)
트랜잭션을 관리하고 제어하는 데 사용됩니다.
트랜잭션은 데이터베이스에서 일련의 작업을 하나의 작업 단위로 처리하는 개념입니다.
주요 명령어
COMMIT: 트랜잭션을 성공적으로 완료하고, 모든 변경 사항을 데이터베이스에 저장합니다.
ROLLBACK: 트랜잭션에서 발생한 모든 변경 사항을 취소합니다.
SAVEPOINT: 트랜잭션 내에서 복구할 수 있는 특정 지점을 설정합니다.
예시.
BEGIN TRANSACTION;
UPDATE Users SET email = 'kobe@example.com' WHERE id = 1;
COMMIT;
3️⃣ SQL의 주요 명령어 예시
1. 데이터 조회(SELECT)
SELECT name, email FROM Users WHERE id = 1;
이 쿼리는 Users 테이블에서 id가 1인 사용자의 이름과 이메일을 조회합니다.
2. 데이터 삽입(INSERT)
INSERT INTO Users (name, email) VALUES ('Kobe', 'kobe@example.com');
이 쿼리는 Users 테이블에 새로운 사용자를 추가합니다.
3. 데이터 수정(UPDATE)
UPDATE Users SET email = 'kobe@example.com' WHERE id = 2;
이 쿼리는 id가 2인 사용자의 이메일을 수정합니다.
4. 데이터 삭제(DELETE)
DELETE FROM Users WHERE id = 3;
이 쿼리는 id가 3인 사용자를 Users 테이블에서 삭제합니다.
4️⃣ SQL의 장점.
1. 데이터 관리의 표준화.
SQL은 대부분의 관계형 데이터베이스에서 사용되는 표준화된 언어입니다.
SQL을 학습하면 여러 데이터베이스 시스템에서 공통적으로 사용할 수 있습니다.
2. 효율적인 데이터 검색.
SQL을 사용하면 데이터베이스 내에서 효율적으로 데이터를 검색할 수 있습니다.
복잡한 조건을 사용하여 대량의 데이터를 빠르게 조회할 수 있습니다.
3. 강력한 데이터 조작.
SQL은 데이터의 삽입, 수정, 삭제와 같은 기본적인 작업 외에도, 데이터의 집계, 정렬, 그룹화 등 강력한 데이터 조작 기능을 제공합니다.
4. 데이터 무결성 보장.
SQL은 데이터의 무결성을 보장하는 다양한 제약 조건(Primary Key, Foreign Key, Unique, Not Null 등)을 정의할 수 있습니다.
5. 대규모 데이터 처리.
SQL은 대량의 데이터를 빠르게 처리하고, 복잡한 쿼리와 연산을 수행할 수 있어 데이터 분석과 비즈니스 인텔리전스 작업에 널리 사용됩니다.
5️⃣ SQL의 단점.
1. 비정형 데이터 처리의 한계.
SQL은 정형화된 테이블 형식의 데이터 처리는 탁월하지만, 비정형 데이터나 유연한 데이터 구조를 처리하는 데는 한계가 있습니다.
이러한 경우 NoSQL 데이터베이스가 더 적합할 수 있습니다.
2. 복잡한 쿼리 작성.
복잡한 데이터 분석이나 대규모 연산을 수행하려면, SQL 쿼리가 매우 복잡해질 수 있습니다.
쿼리의 성능 최적화를 위해 상당한 전문 지식이 필요할 수 있습니다.
6️⃣ SQL의 사용 사례.
1. 웹 애플리케이션
사용자 정보를 저장하고 관리하기 위해 관계형 데이터베이스에서 SQL을 사용합니다.
예를 들어, 전자상거래 사이트에서 사용자 계정, 주문 정보, 제품 데이터를 저장하고 조회하는 데 사용됩니다.
2. 데이터 분석.
비즈니스 인텔리전스(BI) 도구와 함꼐 사용하여 대량의 데이터를 분석하고 보고서를 생성합니다.
SQL은 데이터 분석가가 데이터베이스에서 필요한 데이터를 효율적으로 추출하는 데 필수적인 도구입니다.
3. 트랜잭션 시스템.
은행 시스템, 재고 관리 시스템 등에서는 SQL을 사용하여 데이터의 일관성을 보장하고, 여러 작업을 안전하게 처리하는 트랜잭션 시스템을 구축합니다.
7️⃣ 요약.
SQL(Structured Query Language) 은 관계형 데이터베이스에서 데이터를 관리하고 조작하기 위한 표준 언어입니다.
SQL은 데이터를 조회(SELECT), 삽입(INSERT), 수정(UPDATE), 삭제(DELETE)할 수 있으며, 데이터베이스의 구조를 정의하고 사용자 권한을 관리하는 등 데이터베이스 운영에 필요한 다양한 기능을 제공합니다.
SQL은 관계형 데이터베이스 시스템에서 가장 널리 사용되며, 데이터 관리를 위한 핵심 기술 중 하나입니다.
-
💾 [CS] RDB란?
💾 [CS] RDB란?
RDB는 Relational Databas(관계형 데이터베이스) 를 줄인 용어로, 관계형 모델을 기반으로 데이터를 저장하고 관리하는 데이터베이스 시스템입니다.
RDB에서는 데이터를 테이블 형태로 저장하며, 각 테이블은 행(row) 과 열(column) 로 구성됩니다.
테이블 간의 관계를 키(Key) 를 사용하여 정의하고, 이를 통해 데이터를 효율적으로 관리하고 검색할 수 있습니다.
1️⃣ RDB의 주요 특징.
1. 테이블 형식의 데이터 저장.
관계형 데이터베이스는 데이터를 테이블(Table) 로 저장합니다.
테이블은 데이터의 한 유형을 나타내며, 테이블 안에 여러 레코드(행, row)가 저장됩니다.
각 행(row)은 테이블의 데이터 항목을 나타내고, 열(column)은 데이터의 속성을 나타냅니다.
2. 관계(Relation)
RDB에서 테이블 간의 관계는 Primary Key(기본 키) 와 Foreign Key(외래 키) 를 사용하여 정의됩니다.
기본 키(Primary Key)는 각 테이블에서 각 행(row)을 고유하게 식별하는 값이며, 외래 키(Foreign Key)는 다른 테이블의 기본 키(Primary Key)를 참조하는 값입니다.
이 키를 통해 테이블 간의 관계를 설정하고, 데이터의 일관성과 무결성을 보장합니다.
3. SQL(Structured Query Language) 사용.
관계형 데이터베이스는 SQL을 사용하여 데이터를 조작하고 관리합니다.
SQL은 데이터를 삽입, 조회, 수정, 삭제하는 명령어를 제공하며, 데이터베이스의 구조(테이블 생성, 수정, 삭제 등)를 정의하는 언어입니다.
🙋♂️ SQL이란?
4. 데이터 무결성.
RDB는 데이터 무결성(Integrity) 을 보장하기 위한 다양한 제약 조건을 지원합니다.
무결성 제약 조건으로는 Primary Key, Foreign Key, Unique 제약 조건 등이 있습니다.
이러한 제약 조건을 통해 데이터의 정확성과 일관성을 유지할 수 있습니다.
5. ACID 속성.
관계형 데이터베이스는 트랜잭션 처리에서 ACID 속성을 준수합니다.
원자성(Atomicity) : 트랜잭션 내의 모든 작업이 성공적으로 완료되거나, 그렇지 않으면 아무 것도 완료되지 않아야 합니다.
일관성(Consistency) : 트랜잭션이 완료된 후 데이터베이스는 항상 일관된 상태를 유지해야 합니다.
고립성(Isolation) : 동시에 실행되는 트랜잭션들은 서로의 작업에 영향을 주지 않아야 합니다.
지속성(Durability) : 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 데이터베이스에 반영됩니다.
🙋♂️ 트랜잭션
2️⃣ RDB의 주요 개념.
1. 테이블(Table)
테이블은 RDB의 가장 기본적인 저장 단위입니다.
테이블은 행(row)과 열(colum)로 구성되며, 각 행(row)은 하나의 레코드(데이터 항목)를, 각 열(column)은 데이터 속성을 나타냅니다.
예를 들어, Users라는 테이블에는 사용자 정보가 저장됩니다.
예시 Users 테이블
id
name
email
1
Kobe
kobe@example.com
2
MinSeond
minseong@example.com
2. 기본 키(Primary Key)
테이블에서 각 레코드를 고유하게 식별할 수 있는 열입니다.
기본 키는 중복될 수 없으며, null 값을 가질 수 없습니다.
예를 들어, Users 테이블에서 id 열이 기본 키가 될 수 있습니다.
3. 외래 키(Foreign Key)
한 테이블의 열이 다른 테이블의 기본 키를 참조할 때, 이를 외래 키라고 합니다.
외래 키를 통해 두 테이블 간의 관계가 설정됩니다.
예를 들어, Orders 테이블의 user_id 열은 Users 테이블의 id 열을 참조하는 외래 키가 될 수 있습니다.
예시 Orders 테이블 (외래 키를 사용)
order_id
user_id
product
1001
1
Laptop
1002
2
Smartphone
여기서 user_id는 Users 테이블의 id를 참조하는 외래 키(Foreign Key)입니다.
4. 관계(Relation)
테이블 간의 관계는 1:1, 1(일대다), N(다대다) 관계로 정의될 수 있습니다.
1:1 관계 : 한 테이블의 하나의 레코드가 다른 테이블의 하나의 레코드와만 연관됩니다.
1 관계 : 한 테이블의 하나의 레코드가 다른 테이블의 여러 레코드와 연관됩니다.
N 관계 : 두 테이블 간의 여러 레코드가 서로 여러 레코드와 연관됩니다. 이를 처리하기 위해 연결 테이블이 사용됩니다.
3️⃣ RDB의 예시
1. 테이블 생성
CREATE TABLE Users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
2. 데이터 삽입
INSERT INTO Users (id, name, email) VALUES (1, 'Kobe', 'kobe@email.com');
3. 데이터 조회
SELECT * FROM Users WHERE id = 1;
4. 테이블 간의 관계 설정
CREATE TABLE Orders (
order_id INT PRIMARY KEY,
user_id INT,
product VARCHAR(100),
FOREIGN KEY (user_id) REFERENCE Users(id)
);
이 예시에서는 Orders 테이블의 user_id가 Users 테이블의 id를 참조하는 외래 키로 설정됩니다.
4️⃣ RDB의 장점.
1. 데이터 일관성.
관계형 데이터베이스는 SQL을 통해 데이터의 무결성과 일관성을 유지할 수 있습니다.
기본 키(Primary Key)와 외래 키(Foreign Key)를 통해 데이터 간의 관계를 관리하고, 중복 데이터를 방지합니다.
2. 데이터 무결성 보장.
데이터 무결성 제약 조건(Primary Key, Foreign Key, Unique 등)을 통해 데이터베이스는 데이터의 정확성과 일관성을 보장합니다.
3. 표준화된 쿼리 언어.
SQL이라는 표준화된 언어를 사용하여 데이터를 조작하고 관리할 수 있습니다.
다양한 RDBMS에서 SQL을 사용하기 때문에 범용성이 높습니다.
4. 복잡한 쿼리 처리.
관계형 데이터베이스 조인(Join) 연산을 통해 여러 테이블의 데이터를 결합하여 복잡한 쿼리를 처리할 수 있습니다.
5. ACID 트랜잭션 지원.
관계형 데이터베이스는 ACID 속성을 준수하여 트랜잭션의 일관성을 보장합니다.
이는 금융시스템이나 중요한 데이터 처리에 매우 적합합니다.
5️⃣ RDB의 단점.
1. 확장성의 한계.
관계형 데이터베이스는 스케일 아웃(서버를 수평적으로 확장하는 방식)에 제약이 있을 수 있습니다.
대규모 데이터 처리나 복잡한 분산 환경에서는 NoSQL 데이터베이스가 더 적합할 수 있습니다.
2. 복잡한 구조.
테이블 간의 관계를 설정하고 관리하는 것은 강력하지만, 복잡한 스키마 설계가 필요한 경우 유지 관리가 어려워질 수 있습니다.
3. 비정형 데이터 처리의 한계.
관계형 데이터베이스는 고정된 스키마 구조를 가지므로, 비정형 데이터(예: JSON, XML, 미디어 파일 등)를 처리하는 데는 적합하지 않을 수 있습니다.
6️⃣ RDB의 예시 시스템.
1. MySQL
오픈 소스 관계형 데이터베이스로, 웹 애플리케이션에서 가장 많이 사용됩니다.
2. PostgreSQL
고급 기능을 제공하는 오픈 소스 관계형 데이터베이스로, 복잡한 쿼리와 트랜잭션을 지원합니다.
3. Oracle Database
엔터프라이즈급 데이터베이스로, 성능과 안정성이 매우 뛰어나고, 많은 기능을 제공합니다.
4. Microsoft SQL Server
Microsoft에서 개발한 관계형 데이터베이스로, 주로 Windows 환경에서 사용됩니다.
7️⃣ RDB의 활용 사례.
1. 전자상거래 시스템
사용자 정보, 주문 정보, 제품 정보 등을 테이블로 관리하고, 이러한 테이블 간의 관계를 설정하여 데이터를 일관성 있게 관리합니다.
2. 은행 시스템
계좌 정보, 거래 기록, 고객 정보 등을 테이블에 저장하고, 각 테이블 간의 관계를 통해 테이터를 안전하게 관리합니다.
트랜잭션 처리가 중요합니다.
3. ERP 시스템
기업 자원 관리 시스템에서 생산, 재고, 구매, 판매, 인사 등의 데이터를 테이블로 관리하고, 서로 관계를 맺어 데이터를 효율적으로 처리합니다.
8️⃣ 요약.
RDB(Relational Database) 는 데이터를 테이블 형태로 저장하고, 관계(Relation) 를 통해 테이블 간의 연결을 관리하는 데이터베이스 시스템입니다.
RDB는 SQL을 사용하여 데이터를 조회, 삽입, 수정, 삭제하며, ACID 속성을 통해 트랜잭션의 일관성을 보장합니다.
데이터베이스의 무결성을 유지하고 복잡한 쿼리를 처리하는데 강력한 기능을 제공하지만, 확장성이나 비정형 데이터 처리에는 한계가 있을 수 있습니다.
-
💾 [CS] Database란?
💾 [CS] Database란?
데이터베이스(Database) 는 데이터를 구조적이고 체계적으로 저장하고 관리하는 시스템입니다.
데이터베이스는 데이터를 쉽게 저장하고, 검색하고, 수정하고, 삭제할 수 있도록 설계된 소프트웨어 시스템입니다.
주로 다수의 사용자 또는 애플리케이션이 동시에 데이터를 효율적으로 사용할 수 있도록 지원합니다.
1️⃣ 데이터베이스의 주요 특징.
1. 데이터 저장.
데이터베이스는 데이터를 테이블 형식으로 구조화하여 저장합니다.
테이블을 행(row), 열(column)로 구성되며, 각 행은 데이터의 한 레코드를 나타내고, 열은 데이터의 속성을 나타냅니다.
2. 데이터 관리.
데이터베이스는 데이터를 삽입, 수정, 삭제, 검색하는 기능을 제공합니다.
이 과정에서 데이터의 일관성, 무결성, 보안이 보장되도록 관리됩니다.
3. 데이터 무결성
데이터베이스는 데이터의 정확성과 일관성 을 유지하기 위한 다양한 규칙과 제약 조건을 지원합니다.
예를 들어, 중복되지 않는 데이터를 보장하거나, 외부 테이블과의 참조 관계를 유지할 수 있습니다.
4. 동시성 제어.
여러 사용자가 동시에 데이터에 접근할 때, 데이터베이스는 동시성 제어 기능을 통해 여러 사용자가 데이터를 안전하게 읽고 쓸 수 있도록 합니다.
5. 데이터 보안.
데이터베이스는 사용자가 특정 데이터에 접근하거나 수정할 수 있는 권한을 설정하여 데이터를 보호합니다.
이를 통해 민감한 정보는 허가된 사용자만 접근할 수 있도록 할 수 있습니다.
6. 백업 및 복구.
데이터베이스는 데이터 손실을 방지하기 위해 백업 및 복구 기능을 지원합니다.
시스템 장애나 오류가 발생하더라도 데이터가 복구될 수 있도록 설계됩니다.
2️⃣ 데이터베이스의 종류.
1. 관계형 데이터베이스(Relational Database, RDBMS)
관계형 데이터베이스는 데이터를 테이블 형태로 저장하며, 테이블 간의 관계를 설정할 수 있는 데이터베이스입니다.
각 테이블은 고유한 Primary Key(기본 키) 를 통해 다른 테이블과 연결됩니다.
관계형 데이터베이스에서는 SQL(Structured Query Language) 를 사용하여 데이터를 관리합니다.
예시: MySQL, PostgreSQL, Oracle, Microsoft SQL Server, SQLite 등
특징
데이터를 테이블 형태로 저장하며, 행(row) 과 열(cloumn) 로 구성됩니다.
SQL을 사용하여 데이터를 조회하고 조작합니다.
데이터 무결성을 보장하기 위한 제약 조건(Primary Key, Foreign Key 등)을 지원합니다.
예시:
CREATE TABLE User (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE
);
INSERT INTO Users (id, name, email) VALUES (1, 'Kobe', 'kobe@example.com');
SELECT * FROM Users;
2. NoSQL 데이터베이스 (Not Only SQL)
NoSQL 데이터베이스는 비정형 데이터 또는 반정형 데이터를 저장하는 데 적합한 데이터베이스로, 유연한 스키마와 확장성을 강조합니다.
NoSQL 데이터베이스는 다양한 유형의 데이터 모델(문서, 키-값 그래프 등)을 사용하여 데이터를 저장할 수 있습니다.
예시: MongoDB, Cassandra, Redis, Couchbase, Neo4j 등
특징
비정형 데이터나 대규모 데이터를 처리하는 데 적합합니다.
데이터 스키마가 고정되지 않아 유연한 데이터 구조를 지원합니다.
고속 읽기/쓰기와 높은 확장성을 제공합니다.
문서형 데이터베이스(MongoDB 예시)
{
"_id": 1,
"name": "Kobe",
"email": "kobe@example.com"
}
키-값 데이터베이스 (Redis 예시)
SET "name" "Kobe"
GET "name"
3. 클라우드 데이터베이스
클라우드 데이터베이스는 클라우드 환경에서 호스팅되는 데이터베이스로, 사용자가 직접 하드웨어를 관리할 필요 없이 클라우드 서비스 제공자가 데이터베이스를 관리합니다.
클라우드 데이터베이스는 필요에 따라 자원을 확장할 수 있고, 높은 가용성과 자동 백업 등의 장점을 제공합니다.
예시: Amazon RDS, Google Clould SQL, Microsoft Azure SQL Database, MongoDB Atlas 등
4. 그래프 데이터베이스
그래프 데이터베이스는 데이터 간의 관계를 노트(Node)와 엣지(Edge)로 표현하여 저장하는 데이터베이스입니다.
복잡한 관계를 효율적으로 표현하고 탐색할 수 있기 때문에, 소셜 네트워크, 추천 시스템 등에서 자주 사용됩니다.
예시: Neo4j, ArangoDB, Amazon Neptune
특징
데이터를 그래프 구조로 표현하여 복잡한 관계를 효율적으로 관리합니다.
데이터 간의 관계를 중점적으로 처리하는 데 유리합니다.
3️⃣ 데이터베이스의 기본 개념.
1. 테이블(Table)
관계형 데이터베이스에서 데이터를 저장하는 기본 단위입니다.
테이블은 행(row)과 열(column)로 구성되며, 각 행은 개별 데이터 레코드를 나타내고, 열은 데이터의 속성을 나타냅니다.
2. 열(Column)
테이블에서 각 열은 데이터를 설명하는 속성입니다.
예를 들어, 사용자 테이블에서 열은 이름(name), 이메일(email) 같은 속성일 수 있습니다.
3. 행(Row)
테이블에서 각 행은 데이터의 한 레코드(인스턴스)를 나타냅니다.
예를 들어, 사용자 테이블에서 행은 개별 사용자 정보를 나타냅니다.
4. 기본 키(Primary Key)
테이블에서 각 행을 고유하게 식별할 수 있는 값입니다.
Primary Key는 테이블에서 중복되지 않는 값을 가져야 하며, 각 레코드가 고유하게 구분될 수 있도록 합니다.
5. 외래 키(Foreign Key)
테이블 간의 관계를 정의할 때 사용되는 키입니다.
한 테이블의 열이 다른 테이블의 기본 키를 참조하는 방식으로 두 테이블을 연결합니다.
6. SQL(Structured Query Language)
관계형 데이터베이스에서 데이터를 저장, 조회, 수정, 삭제하기 위해 사용하는 표준 언어입니다.
SQL을 사용하면 데이터베이스에 명령을 전달하여 원하는 작업을 수행할 수 있습니다.
SQL의 주요 명령어
SELECT: 데이터를 조회합니다.
SELECT * FROM Users;
INSERT: 데이터를 삽입합니다.
INSERT INTO Users (name, email) VALUES ('Kobe', 'kobe@example.com');
UPDATE: 데이터를 수정합니다.
UPDATE Users SET email = 'new_email@example.com' WHERE id = 1;
DELETE: 데이터를 삭제합니다.
DELETE FROM Users WHERE id = 1;
4️⃣ 데이터베이스 관리 시스템(DBMS)
DBMS(Database Management System) 는 데이터베이스를 관리하고, 데이터의 저장, 수정, 삭제, 검색을 지원하는 소프트웨어 시스템입니다.
DBMS는 데이터의 무결성, 보안, 동시성 제어를 보장하고, 다수의 사용자가 효율적으로 데이터를 관리할 수 있도록 도와줍니다.
DBMS의 역할.
1. 데이터 저장 및 관리.
데이터를 체계적으로 저장하고, 관리하며, 필요한 데이터를 빠르게 검색할 수 있습니다.
2. 데이터 무결성 보장.
데이터의 정확성과 일관성을 유지합니다.
중복 데이터 방지, 외래 키 제약 등을 사용하여 데이터의 무결성을 보장합니다.
3. 보안 관리.
사용자별로 접근 권한을 부여하고, 데이터 접근을 제어합니다.
4. 동시성 제어.
여러 사용자가 동시에 데이터에 접근할 때, 데이터의 일관성을 유지하면서 동시성을 관리합니다.
5. 트랜잭션 관리.
트랜잭션이 성공적으로 완료될 때마 데이터를 반영하고, 실패할 경우 데이터를 원래 상태로 복구합니다.
5️⃣ 데이터베이스의 활용 사례.
1. 웹 애플리케이션.
사용자 정보, 제품 정보, 게시글 등을 저장하는 데 사용합니다.
예를 들어, 전자상거래 사이트에서 상품 정보와 주문 데이터를 저장하고 관리합니다.
2. 은행 시스템.
금융 거래 데이터를 안전하게 저장하고 관리하며, 트랜잭션을 통해 정확성을 보장합니다.
3. 병원 정보 시스템.
환자 정보, 진료 기록, 처방전을 저장하고 관리하는 데 사용됩니다.
4. 소셜 네트워크.
사용자 간의 관계를 저장하고, 뉴스피드, 메시지 등의 데이터를 관리합니다.
6️⃣ 요약.
데이터베이스(Database) 는 데이터를 체계적으로 저장하고 관리하는 시스템입니다.
데이터를 저장하고, 검색하고, 수정하고, 삭제하는 기능을 제공하며, 주로 관계형 데이터베이스(RDBMS)와 NoSQL 데이터베이스로 구분됩니다.
데이터베이스는 효율적인 데이터 관리, 데이터 무결성, 동시성 제어, 보안 등의 기능을 제공하며, 다양한 애플리케이션에서 핵심적으로 사용됩니다.
-
🍃[Spring] 서버에서 데이터를 디스크(Disk)에 저장하는 방법.
🍃[Spring] 서버에서 데이터를 디스크(Disk)에 저장하는 방법.
서버에서 데이터를 디스크(Disk) 에 저장하는 과정은 소프트웨어와 하드웨어가 협력하여 이루어집니다.
일반적으로 Java 백엔드 애플리케이션에서는 파일 시스템에 파일을 저장하거나, 데이터베이스에 데이터를 영구적으로 저장하는 두 가지 주요 방법을 사용하여 디스크에 데이터를 기록할 수 있습니다.
1️⃣ 파일 시스템에 데이터를 저장하는 방법.
파일 시스템을 통해 디스크에 데이터를 저장하는 방식은 파일을 직접 생성하고, 그 안에 데이터를 기록하는 방식입니다.
이를 통해 로그, 이미지 파일, 텍스트 파일 등을 디스크에 저장할 수 있습니다.
파일 저장 예시(Java)
Java에서는 FileOutputStream이나 BuffereWriter 등을 사용하여 파일을 디스크에 저장할 수 있습니다.
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileStorageExample {
public static void main(String[] args) {
String filePath = "/path/to/file.txt"; // 저장할 파일 경로
String data = "This is the content to save in the file";
try (BufferWriter writer = BufferedWriter(new FileWriter(filePath))) {
writer.write(data); // 파일에 데이터를 기록
System.out.println("File saved successfully")
} catch (IOException e) {
e.printStackTrace();
}
}
}
실행 과정
1. FileWriter
Java의 FileWriter는 지정된 경로에 새 파일을 생성하고 데이터를 기록하는 클래스입니다.
만약 파일이 이미 존재한다면, 해당 파일에 덮어쓰기 또는 추가할 수 있습니다.
2. BufferedWriter
데이터의 효율적인 기록을 위해 BufferedWriter를 사용하여, 메모리에서 디스크로 데이터가 효과적으로 전달되도록 합니다.
3. 디스크에 기록
이 코드가 실행되면, 파일은 지정된 디렉토리에 생성되고, 디스크에 데이터를 기록합니다.
2️⃣ 데이터베이스에 데이터를 저장하는 방법.
데이터베이스는 서버 애플리케이션에서 디스크에 데이터를 저장하는 또 다른 주요 방식입니다.
서버에서는 데이터베이스 관리 시스템(DBMS, Database Management System) 을 통해 데이터를 관리하며, DBMS는 데이터를 디스크에 영구적으로 저장합니다.
일반적으로 Java 애플리케이션은 JDBC(Java Database Connectivity) 또는 JPA(Java Persistence API) 같은 ORM(Object Relational Mapping) 프레임워크를 사용해 데이터베이스와 상호작용합니다.
데이터베이스에 데이터 저장 예시(JPA)
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
@Entity
public class User {
@Id
@GeneratedValue(staratege = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
// ...
}
public class DatabaseStorageExample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
User user = new User();
user.setName("Kobe");
user.setEmail("kobe@example.com");
em.persist(user); // 데이터베이스에 저장 (디스크로 저장됨)
em.getTransaction().commit();
em.close;
emf.close();
System.out.printlsn("Data saved to database");
}
}
실행 과정.
1. Entity 정의.
User 클래스는 데이터베이스의 테이블에 매핑된 엔티티(Entity) 입니다.
@Entity 애너테이션을 통해 JPA는 이 객체를 테이블과 연결합니다.
🙋♂️ 엔티티(Entity)
2. EntityManage.
JPA의 EntityManager는 데이터베이스와 상호작용하는 인터페이스로, 데이터를 삽입, 수정, 삭제할 수 있습니다.
3. 데이터 저장.
em.persist(user) 메서드를 통해 User 객체를 데이터베이스에 저장하고, 이 데이터는 결국 디스크에 기록됩니다.
4. 트랜잭션 관리.
데이터베이스에 데이터를 저장할 때는 트랜잭션이 필요합니다.
트랜잭션이 완료되면(commit()), 데이터는 영구적으로 디스크에 저장됩니다.
🙋♂️ 트랜잭션(Transaction)
3️⃣ 디스크에 저장되는 방식.
서버에서는 데이터를 저장하는 것은 여러 하드웨어와 소프트웨어 구성 요소가 협력하여 이루어집니다.
아래는 디스크 데이터를 저장하는 기본적인 과정을 설명합니다.
3.1 애플리케이션에서 데이터 생성.
애플리케이션이 파일 시스템 또는 데이터베이스를 통해 데이터를 저장하려고 하면, 해당 데이터를 메모리(RAM)에 생성합니다.
3.2 데이터 저장 요청.
애플리케이션은 운영 체제(OS)를 통해 디스크로 데이터를 저장하라는 요청을 보냅니다.
이 요청은 시스템 콜(System Call)을 통해 파일 시스템이나 데이터베이스 시스템에 전달됩니다.
3.3 디스크에 기록 (I/O 작업).
운영 체제는 디스크 컨트롤러에게 데이터 쓰기 작업을 요청합니다.
디스크(HDD/SSD) 컨트롤러는 디스크의 특정 블록 또는 섹터에 데이터를 기록합니다.
이 과정에서 버스를 통해 데이터가 CPU, RAM, 디스크 간에 전송됩니다.
SSD의 경우, 플래시 메모리 셀에 데이터를 기록하고, HDD의 경우 자기 디스크 플래터에 자성 변화로 데이터를 기록합니다.
3.4 데이터 캐시 처리.
디스크 쓰기 작업은 느리기 때문에, 운영 체제는 캐시를 사용하여 데이터를 먼저 메모리에 기록한 후, 일정 주기로 디스크에 쓰는 작업을 처리합니다.
이를 버퍼링이라고 합니다.
3.5 디스크 완료 및 확인.
데이터가 디스크에 성공적으로 기록되면, 디스크 컨트롤러는 운영 체제에 성공적으로 데이터를 저장했다는 신호를 보냅니다.
이 신호가 애플리케이션까지 전달되면, 데이터 저장이 완료되었음을 확인할 수 있습니다.
4️⃣ 하드웨어적으로 일어나는 일.
서버에서 데이터를 디스크에 저장하는 하드웨어적인 과정은 다음과 같습니다.
1. CPU와 메모리 상호작용.
애플리케이션이 데이터를 생성하면, CPU는 데이터를 메모리(RAM)에 저장합니다.
메모리에서 디스크로 데이터를 전송하기 위해 운영 체제는 시스템 콜을 통해 I/O 작업을 요청합니다.
2. I/O 버스 통신.
CPU는 I/O 버스를 통해 디스크 컨트롤러에 데이터를 전송합니다.
이때, 디스크가 데이터 쓰기 요청을 받게 됩니다.
3. 디스크에 데이터 기록.
HDD의 경우, 플래터 위의 특정 색터에 읽기/쓰기 헤드를 사용해 자성을 변화시켜 데이터를 기록합니다.
SSD의 경우, 전기적 회로를 이용해 플래시 셀에 데이터를 저장합니다.
이 과정은 매우 빠르게 이루어집니다.
4. 디스크 캐시.
디스크에 쓰기 작업을 요청할 때, 디스크 내부에도 캐시 메모리가 존재해 요청된 데이터를 잠시 보관하고, 실제로 디스크에 기록합니다.
SSD는 이 캐시가 HDD보다 훨씬 빠릅니다.
5. 데이터 검증.
데이터가 디스크에 기록되면 디스크 컨트롤러는 저장이 완료되었음을 CPU에 알립니다.
CPU는 이 정보를 운영 체제에 전달하고, 애플리케이션은 성공적으로 데이터가 저장되었음을 알게 됩니다.
5️⃣ 디스크에 저장할 때 고려사항.
1. 동시성.
여러 요청이 동시에 발생할 때 파일 잠금(Locking) 이나 트랜잭션 처리를 통해 데이터의 일관성과 무결설을 보장해야 합니다.
2. 안정성.
디스크에 저장된 데이터가 손실되지 않도록 데이터 무결성을 보장해야 하며, 장애나 전원 문제에 대비해 백업 및 복구 매커니즘을 준비해야 합니다.
3. 속도.
HDD는 기계적 특성 때문에 쓰기 속도가 느리지만, SSD는 전기적 메모리 셀을 사용하여 훨씬 빠른 쓰기 속도를 제공합니다.
성능이 중요한 시스템에서는 SSD를 사용하는 것이 일반적입니다.
6️⃣ 데이터베이스를 활용한 서버의 데이터를 디스크에 저장하는 방식.
서버에서 데이터베이스를 사용해 데이터를 디스크에 저장하는 과정은 여러 소프트웨어 및 하드웨어 구성 요소가 협력하여 이루어집니다.
데이터베이스는 데이터를 효율적으로 관리하고, 안정적으로 디스크에 저장할 수 있는 시스템을 제공합니다.
아래는 데이터베이스를 사용해 데이터를 디스크에 저장하는 기본적인 과정을 설명합니다.
6.1 애플리케이션에서 데이터 생성.
애플리케이션이 데이터를 데이터베이스에 저장하려고 하면, 해당 데이터를 메모리(RAM)에 생성하고 데이터베이스 쿼리(SQL 명령)를 생성합니다.
이 쿼리는 데이터를 삽입, 수정, 삭제하는 명령일 수 있습니다.
6.2 데이터베이스 저장 요청.
애플리케이션은 데이터베이스 드라이버(JDBC 등)를 통해 데이터베이스 서버에 SQL 쿼리를 전달합니다.
이 요청은 운영 체제를 거쳐 데이터베이스 시스템에 전달됩니다.
6.3 데이터베이스 엔진 처리.
데이터베이스 엔진(DBMS)은 전달된 쿼리를 해석하고, 데이터를 어떻게 디스크에 저장할지 계획을 세웁니다.
DBMS는 먼저 트랜잭션 관리(ACID)를 통해 데이터를 안전하게 저장할 수 있도록 처리합니다.
InnoDB와 같은 스토리지 엔진은 데이터를 영구적으로 디스크에 기록하기 전에 메모리 버퍼에 데이터를 저장해 작업을 준비합니다.
6.4 트랜잭션과 데이터 캐시 처리.
데이터베이스는 데이터가 올바르게 저장되기 전까지 데이터를 메모리 버퍼에 보관하고 트랜잭션을 관리합니다.
트랜잭션이 커밋되면 데이터는 디스크로 기록됩니다.
만약 트랜잭션이 실패하거나 취소될 경루, 데이터는 롤백(rollback) 되어 변경 사항이 반영되지 않도록 합니다.
6.5 데이터 저장 및 완료 확인.
트랜잭션이 성공적으로 완료되면, 데이터베이스는 디스크에 데이터를 기록합니다.
디스크에 데이터가 성공적으로 기록되면, 데이터베이스는 애플리케이션에 성공적으로 저장되었음을 알립니다.
이는 SQL 쿼리에 대한 성공 응답으로 확인됩니다.
7️⃣ 데이터베이스를 활용한 데이터 저장 시 하드웨어적으로 일어나는 일.
서버에서 데이터베이스를 사용해 데이터를 디스크에 저장하는 과정은 하드웨어적인 측면에서 CPU, 메모리, 디스크, 네트워크 카드가 모두 상호작용하며 이루어집니다.
아래는 데이터베이스 저장 시 하드웨어적으로 일어나는 과정입니다.
1. CPU와 메모리 상호작용.
애플리케이션이 데이터베이스에 쿼리를 전달하면, CPU는 이를 처리하고 SQL 명령을 데이터베이스 서버로 전달합니다.
데이터베이스 서버에서 CPU는 쿼리를 해석하고, 메모리(RAM)에서 데이터를 읽어 트랜잭션 버퍼와 메모리 캐시로 관리합니다.
2. I/O 버스 통신.
데이터베이스 서버에서 데이터를 디스크로 기록하기 위해 CPU는 I/O 버스를 통해 디스크 컨트롤러로 데이터를 전송합니다.
이때 메모리 버퍼에 저장된 데이터가 디스크에 기록될 준비를 마칩니다.
3. 디스크에 데이터 기록.
HDD의 경우, 데이터베이스 서버의 스토리지 엔진은 플래터에 데이터를 기록하기 위해 디스크 컨트롤러가 쓰기 작업을 지시합니다.
SSD의 경우, 플래시 메모리 셀에 데이터를 기록하며, 이 과정은 매우 빠르게 이루어집니다.
4. 데이터베이스 캐시.
데이터베이스는 캐시 메모리를 활용하여 자주 접근하는 데이터를 빠르게 처리할 수 있도록 합니다.
디스크에 기록된 데이터는 캐시에 보관되어 빠른 읽기 작업을 지원하며, 쓰기 작업은 일괄적으로 처리될 수 있습니다.
5. 데이터 검증.
데이터가 디스크에 기록된 후, 데이터베이스는 트랜잭션이 성공적으로 완료되었음을 CPU에 알립니다.
CPU는 이를 운영 체제에 전달하고, 애플리케이션은 성공적으로 데이터가 저장되었음을 응답받습니다.
8️⃣ 데이터베이스에 데이터를 저장할 때 고려사항.
1. 동시성 제어.
여러 사용자가 동시에 데이터베이스에 접근할 때 데이터베이스는 락킹(Locking) 또는 트랜잭션(Transaction) 격리 수준을 통해 데이터의 일관성을 보장합니다.
동시성 제어는 데이터가 손상되거나 충돌하지 않도록 하는 중요한 메커니즘입니다.
2. 트랜잭션 관리.
데이터베이스는 ACID(원자성, 일관성, 고립성, 지속성) 속성을 통해 데이터가 안전하게 처리되도록 보장합니다.
트랜잭션은 데이터의 무결성을 보장하기 위해 일련의 작업이 모두 성공하거나, 실패하면 모두 롤백되도록 처리됩니다.
3. 성능 최적화.
데이터베이스의 성능은 인덱스, 쿼리 최적화, 캐시 활용 등을 통해 최적화됩니다.
또한, 디스크 성능(예: SSD)이 중요한 역할을 하며, 읽기/쓰기 작업이 많은 시스템에서는 SSD를 사용하는 것이 성능상 유리합니다.
4. 백업 및 복구.
데이터베이스에 저장된 데이터는 중요한 자산이므로, 정기적인 백업이 필요합니다.
장애가 발생할 경우 데이터베이스는 백업을 통해 데이터를 복구할 수 있어야 하며, 이를 위해 로그 파일 및 스냅샷 등의 기능이 지원됩니다.
5. 보안 및 접근 제어.
데이터베이스는 권한 관리와 접근 제어를 통해 데이터를 보호합니다.
민감한 데이터는 암호화되어 저장될 수 있으며, 허가되지 않은 사용자는 데이터에 접근할 수 없도록 보안이 강화됩니다.
-
🍃[Spring] 서버를 실행시켜 API를 동작시키기까지 일어나는 일
🍃[Spring] 서버를 실행시켜 API를 동작시키기까지 일어나는 일
Java 백엔드 애플리케이션에서 서버를 실행시켜 API를 동작시키기까지는 여러 단계의 과정이 순차적으로 진행됩니다.
이 과정은 주로 Spring Boot와 같은 프레임워크를 사용한 애플리케이션을 기준으로 설명됩니다.
전체 흐름은 서버 시작에서 API 요청 처리까지의 과정으로 나눌 수 있습니다.
🙋♂️ 프레임워크와 라이브러리의 차이점
1️⃣ 서버 시작.
Java 애플리케이션에서 서버를 시작하려면 먼저 서버 애플리케이션이 실행되어야 합니다.
Spring Boot에서는 SpringApplication.run() 메서드를 호출하여 서버 애플리케이션을 시작합니다.
Spring Boot 애플리케이션 시작 예시
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication 애너테이션이 붙은 클래스는 Spring Boot 애플리케이션의 진입점(Entry Point) 역할을 하며, SpringApplication.run() 메서드가 실행되면 내부적으로 톰켓(Tomcat) 과 같은 임베디드 웹 서버가 시작됩니다.
2️⃣ 임베디드 웹 서버 실행.
임베디드 톰켓 서버는 Spring Boot에 포함된 기본 웹 서버로, 개발자가 별도로 서버를 설정하지 않아도 됩니다.
서버가 실행되면 지정된 포트(기본적으로 8080)에서 클라이언트 HTTP 요청을 수신할 준비를 합니다.
이 과정에서 다음과 같은 일이 일어납니다.
서버가 포트를 열고, 클라이언트로부터 들어오는 HTTP 요청을 수신할 수 있게 준비합니다.
Spring Boot 애플리케이션 내에 정의된 컨트롤러와 매핑된 URL을 등록하여 어떤 요청이 들어올 때 어떤 메서드가 처리해야 하는지 설정합니다.
3️⃣ 의존성 주입 및 컴포넌트 스캔.
Spring Boot는 애플리케이션을 시작하면서 의존성 주입(Dependency Injection) 과 컴포넌트 스캔(Component Scan) 을 통해 애플리케이션 내에서 정의된 Bean(객체) 들을 찾아 애플리케이션 컨텍스트(Application Context) 에 등록합니다.
의존성 주입
객체 간의 의존성을 자동으로 주입하여 애플리케이션 내에서 사용하는 객체 간의 상호작용을 설정합니다.
예를 들어, 서비스(Service)가 컨트롤러(Controller)에 자동으로 주입됩니다.
컴포넌트 스캔
@Controller, @Service, @Repository 같은 애너테이션을 통해 정의된 Bean을 찾아서 애플리케이션 컨텍스트에 등록합니다.
4️⃣ URL 매핑 및 라우팅 설정.
Spring Boot는 @RestController와 같은 애너테이션을 사용하여 API 앤드포인트와 HTTP 메서드(GET, POST, PUT, DELETE) 를 매핑합니다.
서버가 시작되면, URL 요청이 들어올 때 어떤 메서드가 호출될지 라우팅 정보가 설정됩니다.
예시: 컨트롤러 설정.
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users")
public List<User> getAllUsers() {
// 비즈니스 로직을 호출하여 사용자 목록을 반환
return userService.getAllUsers();
}
}
/api/users로 들어오는 GET 요청은 getAllUsers() 메서드로 라우팅됩니다.
Spring은 DispatcherServlet이라는 중앙 제어 역할을 하는 서블릿을 통해 클라이언트로부터 들어오는 HTTP 요청을 적절한 컨트롤러로 전달하고 처리합니다.
5️⃣ 클라이언트 요청 처리.
서버가 실행된 후, 클라이언트(예: 웹 브라우저, Postman) 가 HTTP 요청을 보냅니다.
클라이언트가 API 엔드포인트(예: /api/users)로 GET 요청을 보냈을 때, 다음과 같은 일이 발생합니다.
1. HTTP 요청 수신
톰켓 서버는 지정된 포트(예: 8080)에서 HTTP 요청을 수신합니다.
2. DispatcherServlet으로 전달
요청은 Spring의 DispatcherServlet으로 전달됩니다.
DispatcherServlet은 중앙 제어 역할을 하며, 요청을 적절한 컨트롤러로 라우팅하는 역할을 합니다.
3. 컨트롤러 메서드 실행
요청된 URL과 HTTP 메서드(GET, POST 등)에 맞는 컨트롤러 메서드가 호출됩니다.
예를 들어, /api/users로 들어온 GET 요청은 getAllUsers() 메서드를 호출합니다.
4. 비즈니스 로직 실행
컨트롤러는 비즈니스 로직을 처리하기 위해 서비스 계층을 호출합니다.
서비스 계층에서 데이터베이스 접근을 필요로 하는 경우, 서비스는 Repository를 호출하여 데이터를 가져옵니다.
5. 응답 생성
컨트롤러는 처리된 결과를 JSON 형식으로 변환하여 클라이언트에 응답을 반환합니다.
Spring Boot는 자동으로 Java 객체를 JSON으로 직렬화(Serialization)하여 클라이언트에 반환합니다.
예시: 요청 및 응답.
클라이언트 요청.
GET http://localhost:8080/api/users
서버 응답
[
{
"id": 1,
"name": "Kobe",
"email": "kobe@example.com"
},
{
"id": 2,
"name": "MinSeong",
"email": "minseong@example.com"
}
]
6️⃣ 데이터베이스 연동(Optional_
요청에 따라 비즈니스 로직에서 데이터베이스에 접근해야 하는 경우, JPA나 Hibernate와 같은 ORM(Object-Relational Mapping) 프레임워크를 통해 데이터베이스에서 데이터를 읽거나 저장할 수 있습니다.
데이터베이스 연동 예시(Repository 사용)
@Repository
public interface UserRepository extend JpaRepository<User, Long> {
}
서비스 계층에서 UserRepository를 호출하여 데이터베이스에서 데이터를 조회하거나 저장하는 작업을 처리합니다.
7️⃣ 응답 전송 및 종료
컨트롤러에서 반환된 데이터를 DispatcherServlet이 받아서 적절한 HTTP 응답(Response) 으로 변환합니다.
응답 데이터는 JSON으로 변환된 후, HTTP 상태 코드와 함께 클라이언트로 전송됩니다.
예를 들어, 요청이 성공적으로 200 OK 상태 코드와 함께 JSON 데이터가 전송됩니다.
8️⃣ 요약
Java 백엔드 애플리케이션에서 API가 동작하는 과정은 크게 서버 실행, 의존성 주입 및 컴포넌트 스캔, URL 매핑, 클라이언트 요청 처리, 그리고 응답 전송의 단계로 이루어집니다.
클라이언트가 요청을 보내면 서버는 해당 요청을 적절한 컨트롤러 메서드로 라우팅하여 데이터를 처리하고, 그 결과를 응답으로 반환하는 일련의 과정이 수행됩니다.
-
💾 [CS] Java 백엔드 애플리케이션에서 서버를 실행시켜 API를 동작시키기까지 하드웨어적으로 일어나는 일
💾 [CS] Java 백엔드 애플리케이션에서 서버를 실행시켜 API를 동작시키기까지 하드웨어적으로 일어나는 일.
Java 백엔드 애플리케이션에서 서버를 실행시키고 API가 동작하기까지는 소프트웨어와 하드웨어가 긴밀하게 상호작용합니다.
하드웨어 측면에서는 CPU, 메모리(RAM), 디스크, 네트워크 카드(NIC)와 같은 컴퓨터 부품들이 중요한 역할을 하며, 이러한 부품들이 어떻게 작동하는지에 대해 설명하겠습니다.
🙋♂️ 서버를 실행시켜 API를 동작시키기까지 일어나는 일 - Spring
1️⃣ 애플리케이션 실행 및 서버 시작.
1.1 애플리케이션 바이너리 로딩.
디스크(HDD/SDD)
먼저, Java 애플리케이션을 실행하면 디스크(HDD나 SDD)에 저장된 애플리케이션 코드와 라이브러리(JAR 파일 등)가 디스크에서 RAM으로 로드됩니다.
디스크 컨트롤러가 디스크의 특정 섹터에 저장된 프로그램 파일을 찾아서 읽어옵니다.
메모리 컨트롤러를 통해 데이터를 RAM으로 복사합니다.
1.2 JVM 시작.
Java 애플리케이션은 Java Virtual Machine(JVM) 에서 실행되므로, JVM 바이너리도 디스크에서 RAM으로 로드되고, JVM이 시작됩니다.
JVM은 애플리케이션의 바이트코드(.class 파일) 를 읽어들여 실행할 준비를 합니다.
1.3 CPU와 메모리의 역할.
CPU는 이제 메모리에 적재된 JVM과 애플리케이션 코드를 처리하기 시작합니다.
JVM이 바이트코드를 기계어로 변환(JIT 컴파일링) 하여 CPU가 이해할 수 있는 명령으로 전환됩니다.
프로세스와 스레드가 생성되고, CPU는 스레드를 통해 애플리케이션 코드를 순차적으로 실행합니다.
이때, CPU의 레지스터와 캐시 메모리가 자주 사용되는 데이터를 보관하고, RAM에서 직접 가져오는 데이터는 버스를 통해 전달됩니다.
2️⃣ 임베디드 웹 서버 실행 (예: Tomcat)
2.1 네트워크 초기화.
서버 애플리케이션이 시작되면, 네트워크 인터페이스 카드(NIC) 가 네트워크 연결을 설정하고 지정된 포트(예: 8080)를 통해 외부에서 들어오는 HTTP 요청을 받을 수 있도록 준비합니다.
서버는 IP 주소와 포트 번호를 바인딩하고, 네트워크 통신을 위해 소켓(Socket) 을 엽니다.
이는 네트워크 인터페이스 카드와 관련된 하드웨어 레벨에서 이루어집니다.
2.2 멀티스레드 처리.
서버 애플리케이션은 멀티스레딩을 통해 동시 요청을 처리할 수 있도록 합니다.
CPU의 코어와 스레드가 생성되어 여러 사용자의 요청을 동시에 처리합니다.
멀티코어 CPU는 여러 요청을 병렬로 처리하여 성능을 향상시킵니다.
CPU는 컨텍스트 스위칭을 통해 스레드간의 작업 전환을 관리하며, 각 요청에 대해 메모리와 CPU 시간을 할당합니다.
3️⃣ 클라이언트 요청 처리.
3.1 네트워크 인터페이스 카드(NIC)
클라이언트가 API 호출을 통해 서버에 HTTP 요청을 보내면, 이 데이터는 TCP/IP 패킷으로 전송됩니다.
네트워크 인터페이스 카드(NIC) 는 네트워크를 통해 들어오는 요청을 수신하고 이를 처리합니다.
네트워크 카드가 수신한 데이터 패킷을 네트워크 스택을 통해 처리한 후, 애플리케이션 레이어에서 이해할 수 있는 HTTP 요청으로 변환됩니다.
3.2 데이터 패킷 처리
수신된 데이터 패킷은 CPU가 처리하게 됩니다.
여기서 CPU는 네트워크 카드에서 받은 패킷을 메모리(RAM) 에 적재하여 DispatcherServlet으로 전달합니다.
CPU는 요청 처리에 필요한 데이터를 캐시 메모리나 RAM에서 불러와 작업을 시작합니다.
4️⃣ 비즈니스 로직 처리 및 데이터베이스 연동.
4.1 CPU와 RAM 간 데이터 이동
요청이 들어오면 CPU는 메모리(RAM)에서 비즈니스 로직 과 API 엔드포인트에 필요한 정보를 가져와 처리합니다.
비즈니스 로직 실행에 필요한 데이터는 RAM에 저장되며, 데이터베이스와의 상호작용이 필요한 경우 디스크에서 데이터베이스가 동작합니다.
🙋♂️ 비즈니스 로직 vs 비즈니스 규칙
4.2 데이터베이스 접근
데이터베이스는 일반적으로 디스크(HDD/SDD) 에 데이터를 저장합니다.
API가 데이터베이스에 접근하여 데이터를 읽거나 쓰기 위한 작업이 필요할 때, 데이터는 디스크에서 읽어와 RAM으로 로드됩니다.
디스크 컨트롤러는 데이터를 디스크에서 찾아 RAM으로 전송하고, CPU는 이 데이터를 처리한 후 적절한 결과를 반환합니다.
5️⃣ API 응답 및 네트워크 전송.
5.1 CPU와 네트워크 카드 간의 데이터 처리.
비즈니스 로직이 완료되면, CPU는 결과 데이터를 메모리(RAM)에 저장하고, 이 데이터를 다시 TCP/IP 패킷으로 변환합니다.
CPU는 패킷을 생성한 후, 패킷은 다시 네트워크 인터페이스 카드(NIC) 로 전달되어 네트워크를 통해 클라이언트에게 전송됩니다.
NIC는 패킷을 인터넷으로 보내는 작업을 처리하며, 클라이언트가 요청한 데이터를 다시 전달합니다.
5.2 클라이언트 응답 전송.
클라이언트는 서버에서 전송된 HTTP 응답을 받게 되고, 서버는 해당 요청 처리에 대한 상태 코드와 데이터를 전달합니다.
이 과정에서 네트워크 카드와 메모리, CPU가 지속적으로 데이터를 주고받습니다.
6️⃣ 하드웨어 관련 핵심 역할 요약.
1. CPU
프로그램의 명령을 처리하고 계산을 수행하는 가장 중요한 장치입니다.
요청을 처리하는 스레드 생성, 데이터 처리, 메모리 관리 등을 담당합니다.
2. RAM
데이터를 일시적으로 저장하고, CPU가 빠르게 접근할 수 있도록 도와줍니다.
요청 처리 중 필요한 데이터는 RAM에 저장되며, CPU가 이 데이터를 불러와 처리합니다.
3. 디스크(HDD/SSD)
애플리케이션 코드와 데이터베이스를 저장하는 영구적인 저장소입니다.
필요한 데이터는 디스크에서 RAM으로 불러와 사용됩니다.
4. 네트워크 인터페이스 카드(NIC)
클라이언트의 요청을 받아들이고, 서버의 응답을 클라이언트에게 보내는 역할을 담당합니다.
네트워크 패킷을 주고받으며 인터넷 상에서 데이터가 흐르게 합니다.
7️⃣ 전체 과정 요약.
1. 애플리케이션 시작.
디스크에서 애플리케이션이 로드되고, JVM이 메모리에서 실행됩니다.
2. 네트워크 설정.
서버는 포트와 IP 주소를 바인딩하고 외부 요청을 대기합니다.
3. 클라이언트 요청.
클라이언트의 요청이 네트워크를 통해 서버로 전달되고, 네트워크 카드와 CPU가 이를 처리합니다.
4. 비즈니스 로직 실행.
CPU는 요청에 맞는 비즈니스 로직을 실행하고, 데이터베이스나 캐시에서 데이터를 불러와 처리합니다.
5. 응답 생성.
처리된 데이터는 다시 클라이언트로 전송되며, 네트워크 카드와 CPU가 이 과정을 수행합니다.
하드웨어적 측면에서는 디스크, RAM, CPU, 네트워크 인터페이스 카드가 상호작용하며, 각 장치는 자신의 역할을 수행하여 API가 원활히 동작할 수 있도록 지원합니다.
-
💾 [CS] CPU, RAM, DISK
💾 [CS] CPU, RAM, DISK
1️⃣ CPU
CPU(Central Processing Unit) 는 중앙 처리 장치로, 컴퓨터의 두뇌 역할을 하는 핵심적인 부품입니다.
CPU는 컴퓨터에서 수행되는 모든 연산과 명령을 처리하며, 다양한 프로그램이 실행될 수 있도록 하는 중요한 하드웨어입니다.
모든 컴퓨터나 전자 장치는 CPU를 통해 연산, 논리 처리, 제어 기능을 수행합니다.
1. CPU의 주요 역할.
CPU는 프로그램이나 명령어를 받아들이고, 이를 처리하여 결과를 출력하는 과정을 수행합니다.
이 과정에서 연산, 논리적 비교, 데이터 이동 등의 작업을 처리합니다.
CPU는 크게 세 가지 주요 기능을 담당합니다.
1. 연산(Arithmetic and Logic Unit, ALU)
산술 연산(덧셈, 뺄셈, 곱셈, 나눗셈)과 논리 연산(AND, OR, NOT, 비교 등)을 수행하는 역할을 합니다.
모든 계산 작업이 이 부분에서 처리됩니다.
2. 제어(Control Unit, CU)
CPU의 모든 동작을 제어하고 명령어의 실행을 지시합니다.
프로그램 카운터(Program Counter)에 있는 명령을 가져와 해석하고, 해당 명령을 실행하도록 CPU의 다른 구성 요소를 지시합니다.
3. 레지스터(Register)
CPU 내부의 매우 빠른 메모리로, 데이터를 임시로 저장하는 공간입니다.
연산에 필요한 값이나 중간 결과값을 저장하며, 명령어 실행에 필요한 정보들을 저장합니다.
2. CPU의 구성 요소.
1. 코어(Core)
코어는 CPU가 명령어를 처리할 수 있는 단위입니다.
CPU는 하나 이상의 코어를 가질 수 있으며, 싱글 코어(Single-core) CPU에서 멀티코어(Multi-core) CPU로 발전해 왔습니다. 듀얼코어(Dual-core), 쿼드코어(Quad-core), 옥타코어(Octa-core) 등 CPU 코어의 수가 많을수록 여러 작업을 동시에 병렬로 처리할 수 있는 능력이 높아집니다.
2. 클럭 속도(Clock Speed)
CPU의 동작 속도를 나타내며, GHz(Gigahertz) 단위로 표현됩니다.
예를 들어, 3GHz CPU는 초당 30억 번의 명령어를 처리할 수 있는 능력을 가지고 있습니다.
클럭 속도가 높을수록 CPU의 처리 능력이 빠릅니다.
3. 캐시(Cache)
CPU 내부에 있는 고속 메모리로, 자주 사용되는 데이터를 임시로 저장하여 메모리 접근 시간을 단축합니다.
CPU는 L1 캐시 L2 캐시, L3 캐시와 같은 여러 레벨의 캐시를 가지고 있으며, 각 레벨은 용량과 속도에 차이가 있습니다.
4. 버스(Bus)
CPU와 다른 컴퓨터 부품(메모리, 하드 디스크, 그래픽 카드 등) 간에 데이터를 주고받는 통로입니다.
CPU는 메모리 버스를 통해 메인 메모리에서 데이터를 가져오고, 다른 장치들과 데이터를 주고받습니다.
3. CPU의 동작 원리.
CPU는 다음과 같은 단계로 작동합니다.
1. 명령어 인출(Fetch)
CPU는 메모리에서 실행할 명령어를 읽어옵니다.
이때 프로그램 카운터(PC)는 다음에 실행할 명령어의 위치를 가리킵니다.
2. 명령어 해석(Decode)
CPU는 읽어온 명령어를 해석하여, 어떤 작업을 수행해야 하는지를 파악합니다.
해석된 명령어는 ALU나 메모리와 같은 CPU와 다른 구성 요소로 전달됩니다.
3. 명령어 실행(Execute)
해석된 명령어에 따라 연산이 실행됩니다.
예를 들어, 두 수를 더하라는 명령어가 있다면, ALU가 이를 처리하고 결과값을 계산합니다.
4. 결과 저장(Store)
연산된 결과는 레지스터에 저장되거나 메모리로 보내져 저장됩니다.
이 과정은 초당 수십억 번씩 반복되며, 프로그램이 실행됩니다.
3. CPU의 중요성.
CPU는 컴퓨터의 핵심 부품으로, 모든 프로그램의 실행과 데이터 처리를 담당합니다.
CPU의 성능이 좋을수록 컴퓨터의 전반적인 처리 속도가 빠르고 효율적입니다.
CPU는 여러 작업을 동시에 처리할 수 있는 능력인 멀티태스킹을 가능하게 하며, 멀티코어 기술을 통해 병렬 처리가 가능해졌습니다.
4. CPU 성능에 영향을 미치는 요소.
1. 코어 수: 더 많은 코어가 있을수록 병렬로 더 많은 작업을 처리할 수 있습니다.
2. 클럭 속도: 클럭 속도가 빠를수록 명령어를 처리하는 속도가 빠릅니다.
3. 캐시 메모리: 더 큰 캐시 메모리는 자주 사용하는 데이터를 더 빨리 처리할 수 있게 합니다.
4. 아키텍처: CPU의 설계 방식에 따라 성능이 좌우 됩니다. 최신 CPU는 이전 세대 CPU보다 더 효율적으로 명령어를 처리할 수 있는 개선된 아키텍처를 사용합니다.
4. CPU 종류.
1. 데스크탑/노트북용 CPU
Intel: Core i3, i5, i7, i9 시리즈
AMD: Ryzen 3, 5, 7, 9 시리즈
2. 모바일용 CPU
스마트폰이나 테블릿에 사용되는 CPU로는 Qualcomm의 Snapdragon, Apple의 A 시리즈, Samsung의 Exynos등이 있습니다.
3. 서버용 CPU
고성능 서버에서 사용되는 CPU로, Inter Xeon이나 AMD EPYC등이 있습니다.
5. CPU와 GPU의 차이점.
CPU(Central Processing Unit)
범용 연산을 담당하며, 복잡한 명령어를 빠르게 처리할 수 있습니다.
단일 스레드 작업과 논리적인 처리에 강합니다.
GPU(Graphics Processing Unit)
그래픽 처리에 특화된 장치로, 병렬 처리가 가능한 수천 개의 코어를 사용해 대량의 데이터를 동시에 처리할 수 있습니다.
그래픽 처리와 과학적 계산 같은 병렬 연산에 뛰어납니다.
6. 요약.
CPU는 컴퓨터의 핵심 장치로, 프로그램의 명령을 처리하고 연산을 수행하는 역할을 합니다.
코어, 클럭 속도, 캐시 등의 요소에 따라 성능이 좌우되며, 이를 통해 컴퓨터가 다양한 작업을 빠르게 처리할 수 있습니다.
CPU는 모든 컴퓨터의 기본이 되는 연산 장치로서, 성능이 뛰어날수록 컴퓨터의 전반적인 처리 능력이 향상됩니다.
2️⃣ RAM(Random Access Memory)
RAM(Random Access Memory) 는 임의 접근 메모리로, 컴퓨터에서 데이터를 일시적으로 저장하고 처리하는 데 사용되는 고속 메모리입니다.
RAM은 현재 실행 중인 프로그램과 그 프로그램이 사용하는 데이터를 일시적으로 저장하며, CPU가 작업을 빠르게 처리할 수 있도록 도와줍니다.
RAM은 휘발성 메모리로, 전원이 꺼지면 저장된 데이터는 사라집니다.
1. RAM의 역할.
1. 임시 저장소.
RAM은 현재 실행 중인 프로그램이나 작업에 필요한 데이터를 일시적으로 저장합니다.
예를 들어, 사용자가 문서 편집 프로그램을 실행하면, 프로그램과 문서 데이터는 RAM에 저장됩니다.
이렇게 하면 CPU가 필요한 데이터를 빠르게 읽고 쓸 수 있습니다.
2. 빠른 데이터 접근.
RAM은 매우 빠른 속도로 데이터를 읽고 쓸 수 있습니다.
하드 디스크나 SSD 같은 저장 장치에 비해 훨씬 빠르기 때문에, 프로그램이 실행되는 동안 CPU가 RAM에 저장된 데이터를 효율적으로 처리할 수 있습니다.
3. 다중 작업 지원.
RAM은 여러 프로그램을 동시에 실행할 수 있도록 지원합니다.
많은 프로그램이 실행될수록 RAM이 더 많이 필요하며, 충분한 RAM이 있으면 다중 작업(멀티태스킹)이 원활하게 이루어집니다.
2. RAM의 작동 원리.
1. 프로그램 로드.
컴퓨터가 실행되면, 운영 체제(OS)와 사용자가 실행한 프로그램은 하드 디스크나 SSD 같은 영구 저장 장치에서 RAM으로 복사됩니다.
프로그램이 RAM에 로드되면, CPU는 필요한 데이터를 RAM에서 빠르게 읽고 쓸 수 있습니다.
2. 데이터 접근.
RAM의 임의 접근이라는 특성 덕분에, 메모리의 어느 위치에 저장된 데이터든 동일한 시간에 접근할 수 있습니다.
이것이 순차 접근 방식(예: 테이프 드라이브)과의 주요 차이점입니다.
3. 데이터 소멸.
RAM은 휘발성 메모리이기 때문에 전원이 꺼지면 모든 데이터가 사라집니다.
따라서 RAM은 영구적인 데이터 저장을 위한 장치가 아니며, 컴퓨터의 성능을 높이는 데 사용됩니다.
3. RAM의 유형.
1. DRAM(Dynamic RAM)
가장 일반적으로 사용되는 RAM 유형으로, 동적 RAM은 데이터를 유지하기 위해 주기적으로 새로 고침(refresh)이 필요합니다.
DRAM은 컴퓨터의 메인 메모리로 사용되며, 가격이 저렴하고 용량이 큽니다.
2. SRAM(Static RAM)
정적 RAM은 데이터를 저장하기 위해 새로 고침이 필요하지 않으며, DRAM보다 빠르지만 더 비쌉니다.
주로 CPU 캐시와 같은 고속 메모리로 사용됩니다.
3. SDRAM(Symchronous DRAM)
동기식 DRAM은 CPU 클럭 속도와 동기화되어 동작하는 RAM입니다.
일반적인 PC의 메모리는 SDRAM이거나 그 후속 기술인 DDR(Double Data Rate) 메모리입니다.
4. DDR(Double Data Rate)
DDR RAM은 데이터를 한 번에 두 번 전송할 수 있는 메모리로, 현재는 DDR4와 DDR5가 주로 사용됩니다.
이들은 각각 이전 세대에 비해 더 빠른 속도와 더 높은 대역폭을 제공합니다.
4. RAM 용량의 중요성.
RAM 용량은 컴퓨터에서 실행할 수 있는 작업의 수와 복잡성에 큰 영향을 미칩니다.
용량이 클수록 동시에 더 많은 프로그램을 실행하거나 더 많은 데이터를 처리할 수 있습니다.
RAM이 부족하면 컴퓨터는 하드 드라이브에 있는 가상 메모리를 사용하게 되는데, 이는 매우 느리므로 컴퓨터의 성능이 저하됩니다.
권장 RAM 용량.
일반적인 사용 (웹 브라우징, 문서 작업) : 8GB
멀티태스킹(동영상 편집, 여러 프로그램 사용) : 16GB 이상
게임, 그래픽 작업, 프로그래밍: 16 ~ 32GB 이상
5. RAM과 저장 장치의 차이점.
RAM은 데이터를 일시적으로 저장하며, 매우 빠른 속도로 데이터에 접근할 수 있습니다.
그러나 전원이 꺼지면 모든 데이터가 사라집니다.
저장 장치(하드 드라이브, SSD) 는 데이터를 영구적으로 저장하는 장치로, 전원이 꺼져도 데이터가 유지됩니다.
하지만 저장 장치는 RAM보다 데이터 접근 속도가 훨씬 느립니다.
6. RAM과 CPU의 관계.
CPU는 컴퓨터에서 연산과 명령어 처리를 담당하지만, 자체적으로 데이터를 저장하는 능력은 제한적입니다.
따라서 CPU는 RAM에 저장된 데이터를 불러와 작업을 수행합니다.
RAM이 충분하고 많고 빠르면 CPU가 작업을 더 효율적으로 처리할 수 있습니다.
RAM이 부족하면 CPU는 느린 저장 장치에서 데이터를 가져와야 하기 때문에 성능이 저하됩니다.
7. RAM의 예시.
1. 웹 브라우징.
여러 탭을 동시에 열 때마다 각 탭의 데이터는 RAM에 저장됩니다.
탭이 많을수록 RAM을 더 많이 사용하게 됩니다.
2. 비디오 편집.
대용량 비디오 파일을 편집할 때, 편집 프로그램은 파일의 일부를 RAM에 로드하여 빠르게 처리합니다.
RAM 용량이 크면 더 많은 데이터를 한 번에 처리할 수 있습니다.
3. 게임.
현대 게임은 그래픽과 데이터 처리가 많기 때문에 많은 양의 RAM이 필요합니다.
게임은 실행하는 동안 그래픽과 게임 데이터는 RAM에 로드되어 빠르게 처리됩니다.
8. 요약.
RAM(Random Access Memory) 은 컴퓨터에서 데이터를 일시적으로 저장하고, 프로그램 실행 중에 빠르게 접근할 수 있는 고속 메모리입니다.
RAM은 현재 실행 중인 프로그램과 데이터가 저장되며, CPU가 이 데이터를 빠르게 처리할 수 있도록 돕습니다.
RAM은 휘발성 메모리이므로 전원이 꺼지면 데이터가 사라집니다.
RAM 용량과 속도는 컴퓨터 성능에 중요한 영향을 미치며, 충분한 RAM이 있을 경우 멀티태스킹과 복잡한 작업을 원활하게 수행할 수 있습니다.
3️⃣ DISK
디스크(Disk) 는 데이터를 영구적으로 저장하는 장치로, 컴퓨터에서 파일, 프로그램, 운영체제 등 모든 데이터를 장기적으로 보관하는 데 사용됩니다. 디스크는 전원이 꺼져도 데이터를 유지하며, 데이터를 저장하고 불러오는 데 사용됩니다. 디스크는 크게 HDD(Hard Disk Drive) 와 SSD(Solid State Drive) 로 나뉩니다.
디스크의 종류
HDD (Hard Disk Drive):
HDD는 전통적인 자기 디스크 방식으로 데이터를 저장하는 장치입니다. HDD는 여러 개의 플래터(Platter)라는 원판 위에 자성을 이용하여 데이터를 기록하고 읽어옵니다.
플래터는 고속으로 회전하며, 그 위를 지나가는 읽기/쓰기 헤드가 데이터를 읽고 씁니다.
HDD의 주요 특징:
기계적 구조: 플래터와 읽기/쓰기 헤드 같은 기계적 부품을 사용합니다.
대용량: 상대적으로 저렴한 가격으로 대용량 저장 공간을 제공합니다.
속도: SSD에 비해 데이터 접근 속도가 느립니다. 특히 무작위 읽기/쓰기 작업에서 성능이 떨어집니다.
수명: 기계적 부품으로 인해 마모되기 쉬워, 고장 확률이 높을 수 있습니다.
SSD (Solid State Drive):
SSD는 플래시 메모리를 사용하여 데이터를 저장하는 저장 장치입니다. SSD는 기계적 부품이 없고, 전자 회로로만 구성되어 있어 데이터를 빠르게 읽고 쓸 수 있습니다.
HDD와 달리 SSD는 전자적으로 데이터를 처리하므로, 물리적 움직임이 없어 더 빠르고 내구성이 높습니다.
SSD의 주요 특징:
전자식 구조: 플래시 메모리를 사용하여 데이터를 저장합니다.
속도: HDD보다 훨씬 빠른 읽기/쓰기 속도를 제공합니다. 특히 무작위 읽기/쓰기 성능이 뛰어나며, 시스템 부팅, 프로그램 실행 속도가 매우 빠릅니다.
내구성: 기계적 부품이 없으므로 충격에 강하고 내구성이 뛰어납니다.
가격: HDD에 비해 가격이 높지만, 성능 대비 가격이 계속해서 하락하고 있습니다.
저장 용량: 고용량 SSD는 가격이 높으므로, HDD에 비해 용량 대비 가격이 비쌀 수 있습니다.
디스크의 주요 역할
데이터 영구 저장:
디스크는 컴퓨터의 데이터를 영구적으로 저장하는 장치입니다. 사용자가 저장한 파일, 운영체제, 애플리케이션 등은 디스크에 저장되며, 전원이 꺼져도 데이터는 유지됩니다.
프로그램 실행:
사용자가 프로그램을 실행하면, 디스크에 저장된 프로그램과 관련된 데이터가 RAM으로 로드되어 CPU에서 처리됩니다. 이 과정에서 디스크는 필요한 데이터를 빠르게 읽어와 RAM에 전달하는 역할을 합니다.
시스템 부팅:
컴퓨터의 운영체제(OS)는 디스크에 저장되어 있으며, 시스템이 부팅될 때 디스크에서 운영체제 파일을 읽어와 메모리로 로드하여 컴퓨터를 시작합니다.
데이터 읽기/쓰기:
디스크는 데이터를 읽고 쓰는 기능을 통해, 사용자 파일 저장, 애플리케이션 데이터 읽기 등 다양한 작업을 수행합니다. 특히 SSD는 빠른 읽기/쓰기 속도로, 프로그램 실행 속도와 시스템 반응성을 크게 향상시킵니다.
디스크의 주요 성능 지표
용량 (Capacity):
디스크는 GB(기가바이트), TB(테라바이트) 단위로 저장 용량을 측정합니다. 용량이 클수록 더 많은 데이터를 저장할 수 있습니다.
읽기/쓰기 속도 (Read/Write Speed):
디스크의 성능은 데이터를 얼마나 빠르게 읽고 쓸 수 있는지에 따라 결정됩니다. SSD는 HDD에 비해 훨씬 빠른 읽기/쓰기 속도를 자랑합니다.
RPM (Revolutions Per Minute):
HDD의 경우, 플래터의 회전 속도를 나타내는 단위로 RPM이 사용됩니다. RPM이 높을수록 더 빠르게 데이터를 읽고 쓸 수 있습니다. 일반적으로 5400RPM, 7200RPM 등의 HDD가 사용됩니다.
랜덤 접근 시간:
디스크가 임의의 위치에 있는 데이터를 얼마나 빠르게 접근할 수 있는지 측정합니다. SSD는 기계적 부품 없이 전자적으로 데이터를 읽기 때문에 랜덤 접근 시간이 HDD보다 훨씬 짧습니다.
내구성 (Durability):
SSD는 기계적 부품이 없기 때문에 물리적 충격에 강하고 내구성이 뛰어납니다. 반면, HDD는 기계적 동작을 기반으로 하기 때문에 충격에 약하고 고장이 날 가능성이 더 큽니다.
디스크의 장단점
HDD의 장점
저렴한 가격: 같은 용량의 SSD에 비해 훨씬 저렴합니다.
대용량: 대용량 데이터를 저장하기에 유리하며, 테라바이트 단위의 HDD를 저렴한 비용으로 사용할 수 있습니다.
HDD의 단점
느린 속도: SSD에 비해 읽기/쓰기 속도가 느리고, 무작위 접근 성능이 떨어집니다.
기계적 부품의 내구성: 기계적 부품이 고장날 가능성이 있으며, 물리적 충격에 취약합니다.
SSD의 장점
빠른 속도: 부팅 시간, 프로그램 실행 속도, 데이터 읽기/쓰기 속도가 매우 빠릅니다.
내구성: 기계적 부품이 없어 충격에 강하며, 내구성이 뛰어납니다.
조용함: SSD는 기계적 부품이 없기 때문에 소음이 거의 없습니다.
SSD의 단점
비싼 가격: 같은 용량의 HDD에 비해 가격이 더 비쌉니다.
용량 대비 비용: 고용량 SSD는 비싸므로, 대용량 데이터 저장에는 비용이 많이 들 수 있습니다.
디스크의 활용 예
운영체제 설치:
운영체제(OS)는 디스크에 저장되며, 시스템 부팅 시 이 디스크에서 필요한 파일을 로드하여 컴퓨터가 정상적으로 동작합니다.
데이터 저장:
문서, 사진, 동영상, 음악 등 사용자의 파일은 디스크에 저장됩니다. 디스크는 사용자가 데이터를 안전하게 보관하고, 필요할 때 불러올 수 있도록 합니다.
프로그램 실행:
애플리케이션과 관련된 데이터도 디스크에 저장됩니다. 사용자가 프로그램을 실행하면, 프로그램은 디스크에서 데이터를 불러와 RAM으로 이동하여 실행됩니다.
요약
디스크(Disk)는 데이터를 영구적으로 저장하는 장치로, HDD와 SSD가 대표적인 유형입니다.
HDD는 기계적 부품을 사용해 데이터를 저장하며, 대용량 데이터를 저렴하게 저장할 수 있지만 속도가 느립니다.
SSD는 플래시 메모리를 사용하여 빠른 속도와 높은 내구성을 제공하며, 프로그램 실행 및 부팅 속도가 빠릅니다.
디스크는 컴퓨터의 운영체제, 프로그램, 사용자 데이터를 저장하고 관리하는 중요한 역할을 합니다.
-
🍃[Spring] 유일한 식별자(Primary Key)
🍃[Spring] 유일한 식별자(Primary Key).
Java 백엔드 애플리케이션에서 user의 id 정보는 보통 유일한 식별저(Primary Key) 로 사용되며, 각 유저별로 겹치지 않는 고유한 값입니다.
이는 데이터베이스 설계에서 매우 중요한 개념으로, 사용자와 같은 엔티티(Entity)를 고유하게 식별하기 위해 Primary Key를 사용합니다.
1️⃣ Primary Key란?
Primary Key는 데이터베이스 테이블에서 각 행을 고유하게 식별하는 열(Column)입니다.
Primary Key는 테이블 내에서 중복될 수 없으며, null 값을 가질 수 없습니다.
즉, 각 레코드는 Primary Key를 통해 식별되므로, user 테이블에서 각 사용자는 유일한 id를 가져야 하며, 이를 통해 사용자를 구분할 수 있습니다.
2️⃣ 일반적인 예: User 엔티티
user 엔티티에서 id 필드는 주로 Primary Key로 설정되며, 데이터베이스에서 자동으로 생성되거나 애플리케이션에서 생성할 수도 있습니다.
User 엔티티 예시 (JPA를 사용한 경우)
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Primary Key, 유일한 식별자.
private String name;
private String email;
// Getter, Setter, 기본 생성자
public User() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Id
id 필드는 Primary Key임을 나타냅니다.
@GeneratedValue
id 값이 자동으로 생성됨을 의미합니다.
GenerationType.IDENTITY는 데이터베이스에서 자동으로 Primary Key 값을 생성하도록 지정하는 방식입니다.
3️⃣ Primary Key의 역할.
1. 고유성 보장
Primary Key는 각 레코드를 고유하게 식별하므로, 사용자 정보에서 id는 각 유저별로 유일하게 존재하며 절대 중복되지 않습니다.
2. 빠른 조회
Primary Key는 인덱스가 자동으로 생성되기 때문에 데이터베이스에서 해당 레코드를 빠르게 조회할 수 있습니다.
3. 관계 설정
Primary Key는 다른 테이블에서 Foreign Key로 사용되어 테이블 간의 관계를 설정할 수 있습니다.
예를 들어, Order 테이블에서 user_id는 User 테이블의 Primary Key를 참조하여 주문과 사용자를 연결할 수 있습니다.
4️⃣ ID 생성 방식.
ID는 여러 방식으로 생성될 수 있으며, 가장 많이 사용하는 두 가지 방식은 다음과 같습니다.
1. 자동 증가(Auto Increment)
데이터베이스에서 AUTO_INCREMENT 속성을 설정하여 ID가 자동으로 증가합니다.
주로 MySQL, PostgreSQL 같은 데이터베이스에서 사용됩니다.
2. UUID(Universally Unique Identifier)
UUID는 전 세계적으로 고유한 식별자를 생성하는 방식으로, 중복될 가능성이 거의 없습니다.
JPA에서는 UUID를 Primary Key로 사용할 수도 있습니다.
예시
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private String id;
5️⃣ 데이터베이스에서 User 테이블 예시
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);
여기서 id는 Primary Key로, 데이터베이스에서 자동으로 증가하며 각 사용자는 고유한 id 값을 가집니다.
6️⃣ 예시: Primary Key를 통한 사용자 조회
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id " + id));
return ResponseEntity.ok(user);
}
}
위 코드는 id가 Primary Key인 User 엔티티를 데이터베이스에서 조회하는 예시입니다.
id는 고유하기 때문에 이 값을 통해 특정 사용자를 정확하게 조회할 수 있습니다.
7️⃣ 요약.
Java 백엔드 애플리케이션에서 user의 id는 보통 Primary Key로 사용되며, 각 사용자는 고유한 id 값을 가집니다.
Primary Key는 데이터베이스에서 각 레코드를 고유하게 식별하는 필드로, 절대 중복되지 않으며 null 값을 가질 수 없습니다.
Primary Key를 통해 사용자를 빠르고 정확하게 조회할 수 있으며, 테이블 간의 관계를 설정하는 데 중요한 역할을 합니다.
id는 자동 증가 방식이나 UUID 방식으로 생성될 수 있으며, 사용자의 고유성을 보장합니다.
-
🍃[Spring] Controller에서 getter가 있는 객체를 반환하면 JSON이 된다?!
🍃[Spring] Controller에서 getter가 있는 객체를 반환하면 JSON이 된다?!
Controller에서 getter가 있는 객체를 반환하면 기본적으로 JSON으로 변환되어 클라이언트에 전달됩니다.
이 동작은 Spring Framework에서 자동으로 처리해줍니다.
1️⃣ Spring에서 객체가 JSON으로 변환되는 과정.
1. 객체 반환 및 @RestController
Spring에서는 @RestController 또는 @ResponseBody 애너테이션이 붙은 메서드가 객체를 반환하면, 해당 객체는 자동으로 JSON 형식으로 변환되어 클라이언트에 전달됩니다.
Spring은 내부적으로 Jackson이라는 라이브러리를 사용하여 Java 객체를 JSON으로 직렬화합니다.
이 과정에서 객체의 getter 메서드를 통해 데이터를 추출하려 JSON을 생성합니다.
2. Jackson에 의한 직렬화.
Spring Boot는 Jackson을 기본으로 포함하고 있어, 추가적인 설정 없이 Java 객체를 JSON으로 변환할 수 있습니다.
객체의 getter 메서드를 통해 객체 내부의 데이터를 추출하고, 이를 JSON으로 변환합니다.
예를 들어, getName() 이라는 getter가 있으면, 이는 name이라는 필드로 JSON에 포함됩니다.
2️⃣ 예시.
1. 객체 클래스.
public class User {
private Long id;
private String name;
private String email;
// 기본 생성자
public User() {}
// Getter와 Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
2. Controller 클래스.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
// User 객체를 생성하고 반환 (일반적으로는 데이터베이스에서 가져옴)
User user = new User();
user.setId(id);
user.setName("Kobe");
user.setEmail("kobe@example.com");
// 이 객체는 자동으로 JSON 형식으로 변환되어 클라이언트에 반환됨.
return user;
}
}
3️⃣ 동작 원리.
1. 클라이언트 요청.
클라이언트 /users/1 과 같이 요청을 보내면, Spring은 해당 경로에 매핑된 getUserById 메서드를 호출합니다.
2. Java 객체 반환.
이 메서드는 User 객체를 반환합니다.
3. 객체를 JSON으로 변환.
Spring은 @RestController 또는 @ResponseBody를 보고, User 객체를 JSON 형식으로 변환합니다.
이때 Jackson 라이브러리가 getter 메서드들을 사용하여 객체 내부의 값을 추출하고, 이를 JSON으로 직렬화합니다.
🙋♂️ 라이브러리와 프레임워크의 차이점
4. 클라이언트에 JSON 응답.
직렬화된 JSON 데이터가 클라이언트에 응답으로 전송됩니다.
결과로 클라이언트가 받는 JSON
{
"id": 1,
"name": "Kobe",
"email": "kobe@example.com"
}
4️⃣ 주의 사항.
객체에 getter가 있어야 함.
Jackson은 객체의 필드를 직접 접근하는 것이 아니라, 기본적으로 getter 메서드를 사용하여 데이터를 추출합니다.
따라서 getter 메서드가 존재해야 합니다.
객체가 null인 경우.
객체가 null이거나, 특정 필드가 null인 경우 그 필드는 JSON에 포함되지 않거나 null로 표현됩니다.
추가 설정.
필요에 따라 Jackson의 직렬화/역직렬화 방식을 커스터마이징할 수 있습니다.
예를 들어, 필드 이름을 변경하거나 특정 필드를 제외하고 싶을 때는 @JsonProperty, @JsonIgnore 같은 Jackson 애너테이션을 사용할 수 있습니다.
5️⃣ 요약.
Java 백엔드 애플리케이션에서 계층형 아키텍처로 구현한 후 Controller에서 getter가 있는 객체를 반환하면, Spring은 해당 객체를 자동으로 JSON 형식으로 변환하여 클라이언트에 응답합니다.
이는 기본적으로 Spring의 Jackson 라이브러리를 통해 이루어지며, 추가적인 설정 없이도 객체의 getter를 통해 JSON으로 직렬화됩니다.
-
💾 [CS] 라이브러리(Library)와 프레임워크(Framework)의 차이점.
💾 [CS] 라이브러리(Library)와 프레임워크(Framework)의 차이점.
라이브러리(Library) 와 프레임워크(Framework) 는 소프트웨어 개발에서 코드를 재사용하기 위한 도구이지만, 둘 사이에는 중요한 차이점이 있습니다.
이 차이점은 제어 흐름과 사용 방식에서 주로 나타납니다.
1️⃣ 라이브러리(Library)
라이브러리(Library) 는 특정 기능을 수행하는 모듈 또는 함수들의 모음으로, 개발자가 필요할 때 선택적으로 호출하여 사용하는 도구입니다.
라이브러리는 개발자가 작성하는 코드에서 필요한 부분만 가져다 사용할 수 있으며, 제어권은 개발자에게 있습니다.
제어 흐름
라이브러리를 사용할 때는 개발자가 원하는 방식으로 라이브러리의 함수를 호출합니다.
즉, 개발자가 주도적으로 코드를 작성하며 필요한 기능만 가져다 씁니다.
유연성
라이브러리는 단순한 도구로 특정 기능을 제공하며, 개발자는 자유롭게 설계와 구조를 결정할 수 있습니다.
예시: Java의 Controller 라이브러리
import java.util.Collections;
import java.util.List;
public class Example {
public static void main(String[] args) {
List<String> list = List.of("Apple", "Banana", "Orange");
Collections.sort(list);
}
}
위 코드에서 Collections.sort()는 라이브러리 함수로, 개발자가 필요할 때 호출하여 정렬 기능을 사용합니다.
주요 특징.
개발자가 라이브러리의 특정 기능을 선택해서 호출.
코드의 제어 흐름은 개발자가 관리.
유연하고 제한이 적음.
특정 기능을 구현하는 작은 단위의 코드 집합.
2️⃣ 프레임워크(Framework)
프레임워크는 애플리케이션의 구조와 제어 흐름을 미리 정해 놓은 일종의 뼈대 또는 틀입니다.
개발자는 프레임워크에서 제공하는 구조에 맞춰 코드를 작성하며, 제어 흐름은 프레임워크에 의해 관리됩니다.
즉, 프레임워크가 개발자의 코드를 호출하는 방식으로 동작합니다.
제어 역전(Inversion of Control, IoC)
프레임워크는 코드의 흐름을 스스로 제어하며, 개발자가 작성한 코드는 필요한 시점에 프레임워크에 의해 호출됩니다.
이를 제어의 역전이라고 부릅니다.
설계 패턴 제공
프레임워크는 애플리케이션 개발을 위한 구조와 설계 패턴을 제공합니다.
개발자는 그 구조에 맞춰 코드를 작성하면 됩니다.
예시: Spring 프레임워크.
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
위 코드는 Spring 프레임워크를 사용한 예시입니다.
여기서 @RestController 와 @GetMapping 같은 에너테이션을 통해 HTTP 요청이 프레임워크에 의해 자동으로 처리되며, 제어 흐름은 Spring 프레임워크가 담당합니다.
주요 특징.
프레임워크가 제어 흐름을 관리하고, 개발자의 코드를 호출.
일정한 구조와 설계 패턴을 제공하여 통일된 방식으로 개발을 진행.
제어 역전(Inversion of Control, IoC)을 통해 프레임워크가 애플리케이션의 실행을 주도.
더 큰 구조와 틀을 제공하며, 라이브러리보다 더 강력한 제약과 규칙이 적용됨.
3️⃣ 라이브러리와 프레임워크의 차이점 요약.
구분
라이브러리(Library)
프레임워크(Framework)
제어 흐름
개발자가 코드의 흐름을 제어
프레임워크가 코드의 흐름을 제어(제어 역전, IoC)
사용 방식
필요에 따라 선택적으로 호출
프레임워크가 제공하는 구조에 따라 개발
유연성
개발자가 설계와 구조를 자유롭게 선택
프레임워크가 설계와 구조를 미리 정의
책임 분배
특정 기능만 제공
전체 애플리케이션 구조를 정의
규모
작은 모듈이나 함수들의 집합
애플리케이션 개발을 위한 큰 틀을 제공
예시
Java의 Collection
Spring
4️⃣ 제어의 역전(Inversion of Control, IoC).
제어의 역전은 프레임워크의 중요한 특징 중 하나입니다.
제어의 역전이란, 프로그램의 흐름을 개발자가 직접 제어하는 것이 아니라, 프레임워크가 제어를 담당한다는 개념입니다.
개발자는 프레임워크가 요청할 때 실행될 코드를 작성할 뿐, 전체적인 프로그램의 흐름은 프레임워크가 담당합니다.
제어 역전의 예시
라이브러리(Library)
개발자가 직접 라이브러리의 함수를 호출하고, 그 결과를 처리합니다.
프레임워크(Framework)
프레임워크가 애플리케이션의 흐름을 제어하고, 필요한 시점에 개발자가 작성한 코드를 호출합니다.
예를 들어, 사용자의 요청이 들어오면 프레임워크가 해당 요청을 적절한 컨트롤러에 전달합니다.
5️⃣ 결론.
라이브러리는 특정 기능을 제공하는 도구로서, 개발자가 필요할 때 호출하여 사용하는 반면, 프레임워크는 애플리케이션의 구조와 제어 흐름을 관리하며, 개발자가 작성한 코드를 프레임워크가 적절한 시점에 호출합니다.
라이브러리는는 더 유연하고, 특정 기능에 집중된 도구인 반면, 프레임워크는 제어의 역전을 통해 애플리케이션 전반의 구조를 제시하며, 더 큰 틀에서 애플리케이션을 관리하고 개발하는 데 도움을 줍니다.
-
💾 [CS] API 설계, 계층형 아키텍처, 트랜잭션, 엔티티(Entity), 비즈니스 로직과 비즈니스 규칙의 차이점.
💾 [CS] API 설계, 계층형 아키텍처, 트랜잭션, 엔티티(Entity), 비즈니스 로직과 비즈니스 규칙의 차이점.
1️⃣ API 설계.
API 설계 는 클라이언트와 서버 간에 데이터를 주고받기 위한 인터페이스(즉, API)를 정의하고 설계하는 과정입니다.
API는 응용 프로그램 간의 상호작용을 가능하게 하며, API 설계는 이러한 상호작용이 효율적이고 사용하기 쉬우며 확장 가능하도록 하는 것을 목표로 합니다.
특히 RESTful API와 같은 웹 API는 웹 애플리케이션, 모바일 애플리케이션, 서비스 간의 통신을 중점적으로 다룹니다.
1. API 설계의 핵심 요소.
API 설계는 사용자와 시스템 간의 명확하고 일관된 커뮤니케이션을 위한 몇 가지 중요한 요소를 고려해야 합니다.
1. 엔드포인트(Endpoint) 정의.
API 엔드포인트는 API가 제공하는 자원(리소스)에 접근하기 위한 URL 경로입니다.
예를 들어 /users 나 /product/123는 각각 사용자 목록에 접근하거나 특정 제품 정보를 가져오는 엔드포인트가 될 수 있습니다.
명확하고 직관적인 URL 경로 를 설계하는 것이 중요합니다.
URL은 해당 리소스를 잘 표현하고 이해하기 쉽게 설계되어야 합니다.
예시
GET /users
사용자 목록을 가져옴.
POST /users
새로운 사용자 생성.
GET /users/{id}
특정 사용자 정보 조회.
PUT /users/{id}
사용자 정보 수정.
DELETE /users/{id}
사용자 삭제.
2. HTTP 메서드.
HTTP 메서드는 리소스에 대해 어떤 동작을 수행할지를 나타냅니다.
주로 사용하는 메서드
GET : 리소스를 조회할 때 사용합니다.
POST : 새로운 리소스를 생성할 때 사용합니다.
PUT : 기존 리소스를 수정할 때 사용합니다.
DELETE : 리소스를 삭제할 때 사용합니다.
PATCH : 리소스의 일부를 수정할 때 사용합니다.
각 엔드포인트와 HTTP 메서드를 조합하여 API의 기능을 구성합니다.
3. 데이터 형식.
API는 데이터를 주고받는 방식으로 JSON 이나 XML 과 같은 형식을 주로 사용합니다.
대부분의 현대 API는 가벼운 JSON 형식을 기본으로 사용합니다.
응답의 데이터 형식은 클라이언트가 쉽게 파싱할 수 있는 구조로 정의되어야 합니다.
예시(JSON 데이터)
{
"id": 123,
"name": "Kobe",
"email": "kobe@example.com"
}
4. 요청 및 응답 구조.
요청(Request) 은 API로 데이터를 보내는 방식입니다.
요청 본문에는 JSON 또는 폼 데이터가 포함될 수 있으며, 쿼리 파라미터나 URL 파라미터로도 데이터를 전달할 수 있습니다.
응답(Response) 은 요청에 대한 서버의 응답으로, 상태 코드와 함께 데이터를 반환합니다.
요청(Request)
POST /users
Content-Type: application/json
{
"name": "kobe",
"email": "kobe@example.com"
}
응답(Response)
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 123,
"name": "Kobe",
"email": "kobe@example.com"
}
5. 상태 코드.
HTTP 상태 코드는 요청에 대한 결과를 나타내며, API 설계에서 중요한 부분을 차지합니다.
성공 여부와 오류를 구분하는 데 사용됩니다.
200 OK : 요청 성공.
201 Created : 리소스 생성 성공.
400 Bad Request : 잘못된 요청.
401 Unauthorized : 인증 실패.
403 Forbidden : 권한 부족.
404 Not Found : 리소스가 존재하지 않음.
500 Internal Server Error : 서버 오류.
응답 예시.
HTTP/1.1 404 Not Found
6. 요청 파라미터.
API에서 클라이언트가 데이터를 서버에 전달하는 방식으로 URL 파라미터, 쿼리 스트링, 요청 본문 등 다양한 방식이 있습니다.
경로 변수(Path Variable) : URL 경로에 포함된 변수(/users/{id})
쿼리 파라미터(Query Parameter) : URL 뒤에 붙는 ?key=value 형식의 파라미터(/users?sort=name)
요청 본문(Request Body) : POST나 PUT 요청에서 데이터를 전송할 때 본문에 JSON이나 폼 데이터를 포함.
7. 보안.
API는 데이터를 보호하기 위해 인증과 권한 부여 기능을 갖추어야 합니다.
OAuth 2.0, JWT(Json Web Token)와 같은 기술을 통해 인증을 처리할 수 있습니다.
HTTPS를 통해 모든 통신을 암호화하는 것도 필수적인 보안 요소입니다.
8. 버전 관리.
API는 시간이 지나면서 업데이트되거나 변경될 수 있기 때문에, 버전 관리를 통해 하위 호환성을 유지하는 것이 중요합니다.
API 버전은 URL에 포함하는 방식으로 관리할 수 있습니다.
예시.
/v1/users : 버전 1의 API
/v2/users : 버전 2의 API
9. 에러 처리.
클라이언트가 요청을 잘못 보냈거나 서버에서 문제가 발생했을 때, 적절한 에러 메시지와 상태 코드를 제공해야 합니다.
이는 클라이언트가 오류를 쉽게 이해하고 대응할 수 있도록 돕습니다.
에러 응답 예시.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "Invalid input data",
"message": "The email field is required"
}
10. 문서화.
API는 명확한 문서화가 필수입니다.
클라이언트가 API를 올바르게 사용할 수 있도록 엔드포인트, 요청 방법, 파라미터, 응답 형식 등을 상세히 기술한 문서가 제공되어야 합니다.
대표적인 API 문서화 도구로는 Swagger(OpenAPI)가 있습니다.
2. API 설계 원칙.
1. 일관성
모든 엔드포인트와 HTTP 메서드 사용에 일관성을 유지합니다.
예를 들어, 리소스 생성은 항상 POST 메서드를, 조회는 GET 메서드를 사용하도록 일관성을 유지해야 합니다.
2. RESTful 디자인
RESTful API 원칙에 따라 리소스를 URL로 나타내고, HTTP 메서드에 따라 행동을 정의합니다.
3. 확장성
API는 확장 가능하도록 설계되어야 하며, 새로운 기능이 추가되거나 데이터 구조가 변경되더라도 기존 사용자에게 영향을 최소화해야 합니다.
4. 사용자 친화적
API는 사용하기 쉽고 명확하게 설계되어야 하며, 직관적인 엔드포인트 구조와 적절한 에러 메시지를 제공해야 합니다.
3. API 설계 도구.
1. Postman
API 테스트 및 요청/응답 시뮬레이션 도구.
클라이언트 요청을 손쉽게 보내보고 응답을 확인할 수 있습니다.
2. Swagger(OpenAPI)
API 문서화와 자동화된 테스트 도구.
API를 정의하고, 클라이언트가 사용할 수 있는 문서를 자동으로 생성해줍니다.
3. Insomnia
Postman과 비슷한 API 테스트 도구로, 사용자 인터페이스가 직관적입니다.
4. 요약.
API 설계는 클라이언트와 서버 간의 데이터 통신 방식을 정의하는 과정으로, 엔드포인트, HTTP 메서드, 데이터 형식, 보안, 상태 코드 등의 요소를 고려해야 합니다.
RESTful API 원칙에 따라 설계되며, 직관적이고 확장 가능해야 하며, 명확한 문서화를 제공해야 합니다.
보안 및 버전 관리를 통해 API의 유지보수와 확장을 쉽게 할 수 있도록 설계하는 것이 중요합니다.
2️⃣ 계층형 아키텍처(Layered Architecture)
계층형 아키텍처(Layered Architecture) 는 소프트웨어 시스템을 여러 계층으로 나누어 설계하는 아키텍처 패턴입니다.
이 패턴은 각각의 계층이 특정한 책임을 가지며, 각 계층은 자신이 맡은 기능을 처리하고 그 결과를 다른 계층에 전달하는 방식으로 작동합니다.
계층형 아키텍처는 코드의 유지보수성, 재사용성 그리고 확장성 을 높이는 데 중요한 역할을 하며, 특히 엔터프라이즈 애플리케이션에서 많이 사용됩니다.
1. 계층형 아키텍처의 특징.
1. 책임 분리(Separation of Concerns).
각 계층은 고유한 책임을 가지며, 다른 계층과는 특정한 방식만으로만 상호작용합니다.
이로 인해 코드의 모듈화가 가능해지고, 각 계층이 독립적으로 개발되고 유지보수될 수 있습니다.
2. 계층 간 상호작용.
각 계층은 하위 계층과만 상호작용합니다.
예를 들어, UI 계층은 서비스 계층과만 상호작용하며, 서비스 계층은 데이터 접근 계층과만 상호작용합니다.
이러한 상호작용 규칙은 계층 간의 결합도를 낮추고, 시스템을 더 유연하게 만들어줍니다.
3. 유지보수성.
계층 간의 책임이 명확히 분리되므로, 특정 계층의 로직이 변경되더라도 다른 계층에 미치는 영향을 최소화할 수 있습니다.
이를 통해 코드의 유지보수가 쉬워집니다.
4. 확장성.
각 계층은 독립적으로 확장 가능합니다.
특정 계층의 기능이 확장되어도 다른 계층에는 영향을 미치지 않으므로, 기능 추가 및 성능 개선이 용이합니다.
2. 계층형 아키텍처의 계층.
일반적으로 계층형 아키텍처는 다음과 같은 계층들로 나누어집니다.
1. 프레젠테이션 계층(Presentation Layer)
사용자 인터페이스와 관련된 모든 기능을 처리하는 계층입니다.
주로 웹 브라우저, 모바일 애플리케이션 등에서 사용자로부터 입력을 받고, 결과를 화면에 출력합니다.
Java 애플리케이션에서는 주로 Controller가 이 계층에 해당하며, HTTP 요청을 받아서 Service 계층에 전달하고, 그 결과를 사용자에게 반환합니다.
2. 서비스 계층(Service Layer)
비즈니스 로직을 처리하는 계층으로, 애플리케이션의 주요 기능을 구현합니다.
프레젠테이션 계층에서 들어온 요청을 처리하고, 데이터베이스와 상호작용하기 위해 데이터 접근 계층에 요청을 전달합니다.
이 계층에서는 주로 트랜잭션 관리 및 복잡한 비즈니스 로직을 처리합니다.
3. 비즈니스 도메인 계층(Domain Layer)
도메인 모델과 비즈니스 로직이 포함된 계층입니다.
이 계층은 애플리케이션의 핵심 개념을 나타내는 엔티티와 비즈니스 규칙을 관리합니다.
비즈니스 도메인 계층은 다른 계층의 영향을 최소화하기 위해 독립적으로 존재하며, 객체 간의 관계 및 상태를 관리하는 역할을 합니다.
4. 데이터 접근 계층(Data Access Layer)
데이터베이스와 상호작용하는 계층입니다.
주로 Repository 또는 DAO(Data Access Object) 패턴을 사용하여 데이터베이스 CRUD(Create, Read, Update, Delete) 작업을 처리합니다.
데이터베이스와 상호작용하는 로직은 이 계층에 집중되며, 비즈니스 로직과 분리되어 있습니다.
5. 외부 시스템 계층(External Layer) (선택적)
외부 API나 다른 시스템과 통신하기 위한 계층입니다.
외부 서비스나 API에 대한 호출은 이 계층에서 이루어집니다.
3. 계층 간 상호작용.
각 계층은 자신의 상위 계층과 하위 계층에만 의존합니다.
프레젠테이션 계층은 서비스 계층에 요청을 전달하고, 서비스 계층은 비즈니스 도메인과 데이터 접근 계층을 사용하여 작업을 처리합니다.
4. 계층형 아키텍처의 예시.
다음은 계층형 아키텍처를 구현한 예시입니다.
// Presentation Layer(Controller)
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}
// Service Layer
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
return new UserDTO(user);
}
}
// Domain Layer
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// getters and setters
}
// Data Access Layer (Repository)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
5. 계층형 아키텍처의 장점.
1. 유지보수 용이
각 계층은 특정 기능에 대한 책임을 가지므로, 수정이 필요한 경우 해당 계층만 수정하면 됩니다.
다른 계층에는 최소한의 영향을 미치므로 유지보수가 쉽습니다.
2. 모듈화
계층별로 기능이 분리되어 있으므로, 각 계층의 기능이 확장되더라도 다른 계층에 영향을 미치지 않기 때문에, 새로운 기능 추가나 성능 개선이 용이합니다.
3. 확장성
계층형 구조는 확장하기 쉽습니다.
특정 계층의 기능이 확장되더라도 다른 계층에 영향을 미치지 않기 때문에, 새로운 기능 추가나 성능 개선이 용이합니다.
4. 테스트 용이성
각 계층이 분리되어 있어, 계층별로 단위 테스트를 작성하기 쉽습니다.
예를 들어, 서비스 계층의 로직을 테스트할 때 데이터베이스에 접근할 필요 없이 Mock 객체를 사용하여 테스트할 수 있습니다.
6. 계층형 아키텍처의 단점.
1. 복잡성 증가
각 계층이 분리되어 있어 코드 구조가 복잡해질 수 있으며, 작은 애플리케이션에서는 불필요하게 복잡한 구조가 될 수 있습니다.
2. 성능 저하
계층 간의 상호작용이 빈번할 경우, 오버헤드가 발생할 수 있습니다.
각 계층을 거쳐 요청이 처리되기 때문에 응답 시간이 길어질 수 있습니다.
3. 추가 작업 필요
계층별로 책임을 나누어 설계하다 보니, 코드량이 증가하고 추가적인 개발 작업이 필요할 수 있습니다.
7. 계층형 아키텍처의 활용 사례
1. 엔터프라이즈 애플리케이션
복잡한 비즈니스 로직과 데이터 처리가 중요한 대규모 애플리케이션에서 자주 사용됩니다.
2. 웹 애플리케이션
프론트엔드와 백엔드 간의 데이터 통신이 중요한 웹 애플리케이션에서 주로 사용됩니다,
예를 들어, 전자 상거래 사이트나 CRM 시스템 같은 애플리케이션에 적합합니다.
7. 요약.
계층형 아키텍처는 소프트웨어 시스템을 여러 계층으로 나누어, 각 계층이 특정한 책임을 가지도록 설계하는 아키텍처 패턴입니다.
프레젠테이션, 서비스, 도메인, 데이터 접근 등의 계층이 있으며, 각 계층은 상위/하위 계층과만 상호작용합니다.
유지보수성, 모듈성, 테스트 용이성 등의 장점이 있지만, 작은 애플리케이션에서는 복잡성이 높아질 수 있습니다.
대규모 애플리케이션 및 엔터프라이즈 시스템에 적합한 아키텍처입니다.
3️⃣ 트랜젝션(Transaction)
트랜젝션(Transaction) 은 데이터베이스 또는 시스템에서 하나의 작은 작업 단위를 말하며, 일련의 작업들이 모두 성공하거나 모두 실패하는 것을 보장하는 작업입니다.
즉, 트랜잭션은 데이터베이스의 상태를 변경하는 여러 작업을 하나의 묶음으로 처리하여, 이 작업들을 모두 성공적으로 완료되면 그 결과가 데이터베이스에 반영되고, 그렇지 않으면 모든 변경 사항이 취소됩니다
1. 트랜젝션의 특징(ACID 특성)
트랜젝션은 ACID라는 네 가지 중요한 속성을 가져야 합니다.
1. 원자성(Atomicity)
트랜젝션 내의 작업은 모두 성공적으로 완료되거나, 전혀 완료되지 않은 상태가 되어야 합니다.
트랜젝션 내의 하나라도 실패하면, 전체 트렌젝션은 실패한 것으로 간주되어 데이터베이스의 상태를 변경하기 이전의 상태로 되돌립니다.
2. 일관성(Consistency)
트랜젝션이 성공적으로 완료된 후에는 데이터베이스가 항상 일관된 상태를 유지해야 합니다.
즉, 트랜잭션의 실행 전과 실행 후의 데이터베이스는 모든 제약 조건을 만족해야 합니다.
3. 고립성(Isolation)
동시에 실행되는 여러 트랜잭션들이 서로 영향을 미치지 않도록 고립된 상태로 실행되어야 합니다.
하나의 트랜잭션이 완료되기 전까지 트랜잭션이 그 중간의 결과를 참조할 수 없으며, 동시에 실행되는 트랜잭션들이 데이터베이스에 일관성 없는 영향을 주지 않아야 합니다.
4. 지속성(Durability)
트랜잭션이 성공적으로 완료된 후, 그 결과는 영구적으로 데이터베이스에 반영되어야 합니다.
시스템에 장애가 발생해도 트랜잭션 결과는 손실되지 않고 유지되어야 합니다.
2. 트랜잭션의 작업 흐름.
트랜잭션의 작업은 주로 두 가지 명령어로 구분됩니다.
1. COMMIT
트랜잭션 내의 작업이 성공적으로 완료되면, COMMIT을 통해 트랜잭션이 데이터베이스에 영구적으로 반영됩니다.
2. ROLLBACK
트랜잭션 내의 작업이 중간에 실패하면, ROLLBACK을 통해 트랜잭션을 시작하기 전의 상태로 되돌리며, 이때 모든 변경 사항은 취소됩니다.
3. 트랜잭션의 예시.
은행 계좌 이체
은행에서 A 계좌에서 B 계좌로 100달러를 이체하는 과정을 생각해 보겠습니다. 이 과정은 여러 단계로 나누어지며, 이들 단계를 하나의 트랜잭션으로 묶습니다.
1. A 계좌에서 100 달러 인출
2. B 계좌에서 100달러 입금
이때 두 작업은 하나의 트랜잭션으로 처리되며, 아래의 두 가지 경우를 고려할 수 있습니다.
정상적인 경우 : 두 단계가 모두 성공하면 트랜잭션이 COMMIT되어 계좌 상태가 갱신됩니다.
실패한 경우 : A 계좌에서 100달러를 인출했지만, B 계좌로 입금하는 과정에서 문제가 발생하면 트랜잭션은 ROLLBACK되어 A 계좌의 상태도 원래대로 되돌아갑니다. 이로 인해 시스템은 일관된 상태를 유지합니다.
트랜잭션 예제(SQL)
BEGIN TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 'A';
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 'B';
COMMIT;
위 SQL 예제는 트랜잭션 내에서 두 개의 UPDATE 문이 실행되며, 마지막 COMMIT 명령이 실행되며 트랜잭션이 성공적으로 완료됩니다.
만약 중간에 오류가 발생하면 ROLLBACK을 통해 변경 사항이 취소될 수 있습니다.
4. Java와 Spring에서의 트랜잭션.
Java 애플리케이션에서 트랜잭션은 보통 데이터베이스와 관련된 작업을 처리할 때 사용됩니다.
Spring Framework는 @Transactional 애너테이션을 통해 트랜잭션을 쉽게 관리할 수 있도록 지원합니다.
예시: @Transactional 사용
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
fromAccount.debit(amount);
toAccount.credit(amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
@Transactional
이 메서드는 트랜잭션 내에서 실행되며, 메서드 내에서 오류가 발생할 경우 모든 변경 사항은 ROLLBACK 됩니다.
성공적으로 완료되면 COMMIT되어 데이터베이스에 영구적으로 반영됩니다.
5. 트랜잭션의 중요성.
1. 데이터 무결성 보장.
트랜잭션은 데이터의 무결성을 보장합니다.
트랜잭션 내에서 발생하는 모든 작업은 성공적으로 완료되지 않으면 원래 상태로 되돌리므로, 일관성 없는 데이터가 데이터베이스에 저장되는 것을 방지합니다.
2. 복잡한 비즈니스 로직 처리.
여러 단계로 이루어진 비즈니스 로직, 특히 금융 거래나 주문 처리 같은 중요한 작업에서는 트랜잭션을 사용하여 데이터의 일관성을 유지하고 오류 발생 시 안전하게 롤백할 수 있습니다.
3. 동시성 제어
여러 사용자가 동시에 데이터베이스에 접근할 때 트랜잭션을 통해 동시성 문제를 방지할 수 있습니다.
고립성을 통해 서로 간섭 없이 작업이 처리되도록 보장합니다.
6. 트랜잭션 격리 수준
트랜잭션의 고립성(Isolation) 은 동시에 실행되는 여러 트랜잭션이 서로에게 미치는 영향을 제어하는 수준을 결정합니다.
트랜잭션 격리 수준에는 다음과 같은 단계가 있습니다.
1. READ UNCOMMITED
다른 트랜잭션이 아직 COMMIT되지 않은 데이터를 읽을 수 있습니다.
가장 낮은 격리 수준이며, 일관성 문제가 발생할 수 있습니다.
2. READ COMMITED
다른 트랜잭션이 COMMIT한 데이터만 읽을 수 있습니다.
일반적인 수준의 격리이며, 대부분의 데이터베이스가 기본적으로 이 수준을 사용합니다.
3. REPEATABLE READ
트랜잭션 내에서 동일한 데이터를 여러 번 읽어도 항상 같은 데이터를 읽을 수 있습니다.
읽는 동안 데이터가 변경되지 않습니다.
4. SERIALIZABLE
가장 높은 격리 수준으로, 트랜잭션이 직렬화되어 실행됩니다.
동시성 문제가 없지만 성능에 영향을 미칠 수 있습니다.
7. 요약.
트랜잭션은 데이터베이스의 일련의 작업을 하나의 단위로 처리하며, 모든 작업이 성공하면 COMMIT, 실패하면 ROLLBACK을 통해 데이터의 일관성을 보장합니다.
ACID 원칙에 따라 트랜잭션은 원자성, 일관성, 고립성, 지속성을 보장합니다.
트랜잭션은 중요한 비즈니스 로직에서 데이터 무결성을 유지하고 오류를 방지하는 중요한 매커니즘입니다.
Java와 Spring에서는 @Transactional 애너테이션을 통해 트랜잭션을 쉽게 관리할 수 있습니다.
4️⃣ 엔티티(Entity)
엔티티(Entity) 는 데이터베이스 또는 애플리케이션의 비즈니스 로직에서 관리해야하는 데이터를 표현하는 객체 또는 클래스입니다.
엔티티는 애플리케이션의 핵심 비즈니스 개념을 나타내며, 주로 데이터베이스의 데이블과 매핑됩니다.
1. 엔티티의 특징.
1. 데이터베이스 테이블과 매핑.
엔티티는 데이터베이스 테이블 레코드(row)와 1:1로 매핑됩니다.
예를 들어, User 엔티티는 데이터베이스의 user 테이블과 매핑되어 사용자를 관리하는 데 사용됩니다.
각 엔티티는 데이터베이스에서 관리되는 실제 데이터를 표현하며, 각 엔티티의 인스턴스는 테이블의 행(row)을 의미합니다.
2. 상태(필드)를 가진 객체.
엔티티는 주로 애플리케이션의 데이터를 나타내는 속성(필드)을 가지고 있습니다.
이 필드는 테이블의 컬럼(Column, 열)에 대응됩니다.
3. 고유한 식별자(Primary Key)
엔티티는 데이터베이스에서 고유하게 식별될 수 있는 식별자(Primary Key) 를 가져야 합니다.
식별자는 각 엔티티 인스턴스를 유일하게 구분하는 값입니다.
4. 영속성(Persistence)
엔티티는 데이터베이스와 같은 영속적인 저장소에 저장되며, 이 저장소에 데이터를 가져오거나 저장할 수 있는 객체입니다.
즉, 엔티티는 데이터베이스에서 지속적으로 관리되고 필요할 때 다시 사용할 수 있습니다.
2. 엔티티의 예시.
1. Java에서의 엔티티.
Spring Data JPA나 Hibernate 같은 ORM(Object-Relational Mapping) 프레임워크에서 엔티티는 주로 클래스에 @Entity 애너테이션을 붙여서 정의합니다.
이 클래스는 데이터베이스 테이블과 직접적으로 매핑됩니다.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Primary Key
private String name;
private String email;
// 기본 생성자
public User() {}
// Getter and Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = idl
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
2. 데이터베이스 테이블과 매핑
위 User 엔티티 클래스는 다음과 같은 데이터베이스 테이블에 매핑됩니다.
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
)
@Entity
이 클래스가 데이터베이스의 엔티티임을 나타냅니다.
이 클래스는 데이터베이스의 테이블과 매핑이됩니다.
@Id
이 필드가 엔티티의 고유한 식별자인 Primary Key 임을 나타냅니다.
@GeneratedValue
데이터베이스에서 자동으로 생성되는 값임을 지정합니다.
GenerationType.IDENTITY는 데이터베이스에서 자동으로 증가하는 ID 값을 사용합니다.
3. 엔티티의 구성 요소.
1. 식별자(Primary Key)
엔티티는 고유한 식별자를 통해 구분됩니다.
예를 들어, User 엔티티의 id 필드는 데이터베이스의 Primary Key에 해당하며, 이를 통해 각 사용자를 고유하게 구분할 수 있습니다.
2. 속성(Attributes)
엔티티는 여러 속성을 가집니다.
속성은 엔티티의 필드로 정의되며, 데이터베이스 테이블의 컬럼에 대응됩니다.
예를 들어 User 엔티티의 name 과 email 필드는 사용자 이름과 이메일을 나타내는 속성입니다.
3. 연관관계(Relationships)
엔티티 간에는 연관 관계가 존재할 수 있습니다.
예를 들어, User 와 Order라는 두 엔티티가 있을 때, 하나의 사용자가 여러 개의 주문을 가질 수 있습니다.
이러한 관계는 JPA에서 @OneToMany, @ManyToOne, @OneToOne, @ManyToMany 등의 애너테이션으로 정의됩니다.
예를 들어, 사용자와 주문의 관계는 다음과 같이 정의될 수 있습니다.
@OneToMany(mappedBy = "user")
private List<Order> ordersl
4. 상태 및 동작.
엔티티는 데이터베이스의 상태를 반영하며, 비즈니스 로직을 처리하는 데 사용될 수 있습니다.
예를 들어, User 엔티티가 사용자에 대한 상태(활성화 여부 등)를 관리하고, 특정 상태에 따른 로직을 처리할 수 있습니다.
4. 엔티티와 DTO의 차이점.
엔티티(Entity)
엔티티는 데이터베이스 테이블과 매핑된 객체로, 데이터베이스와의 상호작용(저장, 조회, 업데이트 등)에 사용됩니다.
보통 데이터베이스의 필드와 1:1로 매핑되며, 비즈니스 로직을 포함하기도 합니다.
DTO(Data Transfer Object)
DTO는 주로 계층 간 데이터 전송을 위한 객체로, 데이터베이스와의 직접적인 연관이 없습니다.
DTO는 전송에 필요한 필드만 포함하고, 엔티티와는 별도의 구조를 가질 수 있습니다.
이는 주로 클라이언트와 서버 간의 데이터를 주고받기 위해 사용됩니다.
예를 들어, 클라이언트로부터 사용자를 생성하는 요청을 받을 때 DTO를 사용하여 필요한 데이터만 받아오고, 이를 엔티티로 변환하여 데이터베이스에 저장할 수 있습니다.
5. 엔티티의 역할.
1. 데이터베이스와의 상호작용.
엔티티는 데이터베이스와의 상호작용을 위한 매핑 객체로, 애플리케이션이 데이터베이스에서 데이터를 저장, 수정, 삭제, 조회할 수 있도록 합니다.
2. 비즈니스 로직 포함.
엔티티는 단순한 데이터 구조뿐만 아니라, 비즈니스 로직을 포함할 수 있습니다.
예를 들어, User 엔티티는 이메일 형식을 검증하거나 특정 조건에 따라 사용자를 활성화 또는 비활성화하는 로직을 포함할 수 있습니다.
3. 영속성 컨텍스트에서 관리.
엔티티는 JPA와 같은 ORM(Object-Relational Mapping) 프레임워크에서 영속성 컨텍스트에 의해 관리됩니다.
즉, 엔티티의 상태는 트랜잭션 동안 영속성 컨텍스트에 의해 추적되며, 트랜잭션이 완료될 때 해당 상태는 자동으로 데이터베이스에 반영됩니다.
6. 엔티티와 ORM(Object-Relational Mapping)
엔티티는 ORM에서 중요한 역할을 합니다.
ORM은 객체 지향 프로그래밍 언어에서 사용하는 객체와 관계형 데이터베이스의 데이터를 매핑해주는 기술입니다.
즉, 엔티티는 ORM에 의해 자동으로 데이터베이스 테이블과 매핑되고, 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있도록 도와줍니다.
Sping Data JPA나 Hibernate 같은 ORM 프레임워크를 사용하면, 엔티티를 통해 SQL 쿼리를 작성할 필요 없이 데이터베이스와 상호작용할 수 있습니다.
7. 요약.
엔티티(Entity) 는 애플리케이션의 중요한 비즈니스 객체로, 데이터베이스의 테이블과 매핑되며 데이터를 관리하는 역할을 합니다.
엔티티는 고유한 식별자(Primary Key) 를 통해 데이터베이스에서 유일하게 구분되며, 속성과 연관관계를 가집니다.
엔티티는 비즈니스 로직을 포함할 수 있으며, 데이터베이스와의 상호작용을 객체 지향적으로 처리하기 위한 핵심 요소입니다.
ORM 프레임워크를 통해 엔티티는 데이터베이스와 매핑되며, 이를 통해 SQL 쿼리 없이도 데이터를 쉽게 관리 할 수 있습니다.
5️⃣ 비즈니스 로직과 비즈니스 규칙의 차이점.
비즈니스 로직과 비즈니스 규칙은 소프트웨어 개발에서 자주 사용되는 용어이며, 둘 다 애플리케이션의 핵심적인 기능을 정의하지만, 서로 다른 개념입니다.
이 둘의 차이점을 명확히 이해하면 소프트웨어 설계를 더욱 체계적으로 할 수 있습니다.
1. 비즈니스 로직(Business Logic)
비즈니스 로직은 애플리케이션이 어떻게 동작해야 하는지를 정의하는 구현 세부 사항입니다.
비즈니스 로직은 주로 데이터 처리를 포함한 구체적인 작업들을 말하며, 애플리케이션의 기능을 실행하는데 필요한 논리입니다.
예시
사용자로부터 입력을 받아 처리하고, 그 결과를 저장하거나 반환하는 작업.
계좌 이체 기능에서, 특정 계좌에서 돈을 빼고, 다른 계좌에 돈을 넣는 과정에서 수행되는 구체적인 계산과 데이터베이스 업데이트.
재고 관리 시스템에서 제품이 출고되면 재고 수량을 줄이고, 재고가 부족할 경우 경고를 보내는 로직.
특징.
프로세스 중심 : 비즈니스 로직은 시스템 내에서 처리되는 전체 비즈니스 프로세스를 구현합니다.
구현 세부사항 : 어떻게 데이터를 처리하고, 어떤 순서로 작업이 진행되어야 하는지 등 구체적인 방법을 정의합니다.
예시 코드(계좌 이체)
public void transfer(Account fromAccount, Account toAccount, BigDecimal amount) {
if (fromAccount.getBalnce().compareTo(amount) < 0) {
throw new InsufficientFundsException("잔액이 부족합니다.");
}
fromAccount.debit(amount);
toAccount.credit(amount);
accountRepository(fromAccount);
accountRepository(toAccount);
}
위 코드에서는 계좌 간의 금액 이체를 처리하는 비즈니스 로직이 구현되어 있습니다.
이 로직은 구체적인 프로세스(잔액 확인, 계좌 차감 및 입금, 데이터 저장)를 다룹니다.
2. 비즈니스 규칙(Business Rule)
비즈니스 규칙은 비즈니스 로직에서 지켜야 할 규칙이나 제약을 의미합니다.
즉, 무엇을 해야 하고 무엇을 하지 말아야 하는지를 정의하는 비즈니스적 요구 사항입니다.
비즈니스 규칙은 회사의 운영 정책, 법적 규제, 계약 조건, 업계 표준 등에서 도출된 규정들로, 애플리케이션의 도메인(업무)에서 어떤 작업이 허용되고 금지되는지 결정합니다.
예시.
계좌 이체 시 잔고가 부족하면 이체가 불가능합니다.
특정 시간대에만 주문을 받을 수 있습니다.
18세 미만은 성인용 제품을 구매할 수 없습니다.
특징.
정책 중심
비즈니스 규칙은 특정 비즈니스 상황에서 허용되는 동작과 금지되는 동작을 정의합니다.
비즈니스 요구 사항
비즈니스 규칙은 도메인 전문가나 비즈니스 팀이 결정하며, 시스템 설계자가 아닌 비즈니스 자체에서 도출된 요구 사항입니다.
독립적
비즈니스 규칙은 비즈니스 로직과는 독립적으로 존재할 수 있으며, 기술적인 구현 방법과는 관계없이 정의됩니다.
예시 코드(잔액 부족 확인)
public class Account {
public boolean hasSufficientFunds(BigDecimal amount) {
return this.balance.compareTo(amount) >= 0;
}
}
이 코드에서는 비즈니스 규칙인 “잔액이 부족하면 이체를 할 수 없다”는 규칙을 구현한 메서드를 정의했습니다.
비즈니스 로직에서 이 규칙을 호출하여 실제로 잔액이 부족한지 확인하고, 필요한 조치를 취합니다.
3. 차이점 요약.
구분
비즈니스 로직(Business Logic)
비즈니스 규칙(Business Rule)
정의
애플리케이션이 어떻게 동작해야 하는지에 대한 구현.
시스템이 지켜야 할 비즈니스적 제약과 요구 사항.
포커스
기능과 프로세스의 실행 방식.
비즈니스 요구사항을 준수하기 위한 규정 및 제약.
주체
주로 개발자나 시스템 설계자가 구현.
도메인 전문가나 비즈니스 담당자에 의해 정의.
예시
데이터 저장, 계산 처리, 트랜잭션 관리 등
나이 제한, 거래 가능 시간, 신용 한도 초과 등
기술적 관점
기술적, 구현적 측면에서 다름.
기술 구현과 독립적, 비즈니스적 규칙을 나타냄.
영향
애플리케이션 동작을 정의.
동작을 제약하거나 허용함.
4. 비즈니스 로직과 비즈니스 규칙의 관계.
비즈니스 규칙은 비즈니스 로직을 통해 실현됩니다.
즉, 비즈니스 로직이 수행될 때 비즈니스 규칙이 적용되어야 합니다.
비즈니스 로직은 시스템의 동작 방식을 정의하며, 비즈니스 규칙은 해당 로직이 동작할 때 지켜야 하는 제약과 조건을 결정합니다.
비즈니스 규칙은 고정된 정책이나 규정이지만, 비즈니스 로직은 이를 적용하여 다양한 프로세스를 실행하는 구체적인 방법입니다.
비즈니스 로직과 규칙의 예시
비즈니스 규칙 : 계좌 잔액이 부족하면 인출할 수 없다
비즈니스 로작 : 계좌 인출 과정에서 잔액 확인, 금액 인출, 기록 저장 등의 구체적인 절차를 처리.
5. 요약.
비즈니스 로직은 애플리케이션에서 어떻게 처리할지를 다루는 구체적인 작업이며, 시스템의 기능을 정의합니다.
비즈니스 규칙은 비즈니스 운영에서 지켜야 할 규정과 제약을 의미하며, 비즈니스의 요구 사항을 시스템에 반영하기 위한 규칙입니다.
비즈니스 로직은 비즈니스 규칙을 준수하면서 시스템이 어떻게 동작해야 하는지를 구현하는 방식으로, 둘은 상호 보완적인 관계를 가집니다.
-
🍃[Spring] HTTP Body, 메타데이터(Metadata), 바이너리 데이터(Binary Data), JSON(JavaScript Object Notation), `@PostMapping` 애너테이션, `@RequestBody` 애너테이션.
🍃[Spring] HTTP Body
1️⃣ HTTP Body.
HTTP Body 는 HTTP 요청 또는 응답에서 실제 데이터를 포함하는 부분을 말합니다.
주로 클라이언트와 서버 간의 데이터 교환을 위한 목적으로 사용되며, 요청 본문(Request Body) 과 응답 본문(Response Body) 으로 나뉩니다.
1. HTTP Body의 구조.
HTTP 메시지의 구성요소.
1. 요청/응답 라인
HTTP 메서드(GET, POST 등)나 상태 코드(200, 404 등)가 포함된 첫 번째 줄입니다.
2. 헤더(Header)
메타데이터(요청/응답의 속성, 데이터 형식, 인코딩 방식 등)를 포함하며, 각 필드가 key-value 형식 으로 나열됩니다.
3. 본문(Body)
실제 데이터가 들어가는 부분입니다.
HTTP Body는 이러한 메시지의 마지막 부분에 위치하며, 클라이언트가 서버로 보내는 데이너타 서버가 클라이언트로 보내는 데이터가 여기에 포함됩니다.
2. HTTP Body의 사용 목적.
요청(Request) Body
클라이언트가 서버로 보내는 데이터.
주로 POST, PUT, PATCH 등의 요청에서 사용됩니다.
예를 들어, 사용자 로그인 정보, 파일 업로드, 새로운 리소스 생성 등의 데이터를 서버로 보낼 때 사용됩니다.
응답(Response) Body
서버가 클라이언트에게 응답할 때, 필요한 데이터를 본문에 포함하여 보냅니다.
예를 들어, API 호출에 대한 결과로 JSON 형식의 데이터를 반환하거나, HTML 페이지를 반환할 수 있습니다.
3. HTTP Body가 없는 경우.
일부 요청은 Body를 포함하지 않기도 합니다.
GET 요청
주로 데이터를 서버로부터 요청하는 데 사용되며 Body가 포함되지 않습니다.
DELETE 요청
리소스를 삭제할 때 사용되며, 일반적으로 Body가 없습니다.
그러나 POST, PUT, PATCH 요청 은 보통 Body에 데이터를 포함하여 전송합니다.
4. HTTP Body의 형식.
HTTP Body는 다양한 데이터 형식을 가질 수 있으며, 데이터 형식은 Content-Type 헤더에 의해 정의됩니다.
자주 사용되는 형식.
1. application/json
JSON 형식의 데이터를 전달할 때 사용합니다.
예시
{
"name": "Kobe"
"email": "kobe@example.com"
}
2. application/x-www-form-urlencoded
HTML 폼 데이터를 URL 인코딩하여 전달할 때 사용됩니다.
폼 필드가 key=value 형식으로 인코딩되어 전송됩니다.
예시
name=Kobe&email=kobe%40example.com
3. multipart/form-data
파일 업로드와 같은 바이너리 데이터가 포함된 폼 데이터를 전송할 때 사용합니다.
각 파트가 여러 부분으로 나뉘어 전송됩니다.
예시
```json
–boundary
Content-Disposition: form-data; name=”name”
Kobe
–boundary
Content-Disposition: form-data; name=”file”; filename=”profile.jpg”
Content-Type: image/jpeg
[바이너리 데이터]
–boundary–
```
4. text/plain
단순한 텍스트 데이터를 전송할 때 사용됩니다.
예시
Hello, this is a plain text message.
5. 기타 바이너리 데이터.
PDF, 이미지, 비디오 등 다양한 미디어 파일이 Body에 포함될 수 있습니다.
Content-Type 헤더에 맞게 데이터가 인코딩됩니다.
5. HTTP Body의 예시.
요청(Request) Body 예시 (POST 요청)
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 51
{
"username": "Kobe",
"password": "12345"
}
헤더에 Content-Type: application/json이 명시되어 있으며, 이는 Body가 JSON 형식임을 나타냅니다.
Body에는 로그인 정보를 포함한 JSON 데이터가 담겨 있습니다.
응답(Response) Body 예시.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
{
"status": "success",
"data": {
"id": 1,
"name": "Kobe",
"email": "kobe@example.com"
}
}
헤더에 Content-Type: application/json이 설정되어 있으며, 서버가 클라이언트에게 JSON 데이터를 반환합니다.
Body에는 서버가 응답으로 보낸 데이터가 JSON 형식으로 포함되어 있습니다.
6. 요약.
HTTP Body는 클라이언트와 서버 간의 데이터를 전송하는 부분입니다.
주로 POST, PUT, PATCH 요청에서 데이터를 전송할 때 사용되며, 서버의 응답에도 데이터가 포함될 수 있습니다.
데이터 형식은 Content-Type 헤더를 통해 정의되며, JSON, HTML, XML, 바이너리 데이터 등 다양한 형식이 가능합니다.
2️⃣ 메타데이터(Metadata)
메타데이터(Metadata) 는 데이터를 설명하는 데이터, 즉 데이터에 대한 추가 정보를 제공하는 데이터입니다.
메타데이터는 특정 데이터가 무엇을 나타내는지, 어떻게 사용되어야 하는지, 그리고 데이터의 속성에 대한 다양한 정보를 담고 있습니다.
이를 통해 데이터를 더 쉽게 이해하고 관리할 수 있게 합니다.
1. 메타데이터의 역할.
메타데이터는 데이터 자체에 대한 설명을 제공함으로써, 데이터의 구조, 의미,목적 등을 명확히 알려줍니다.
메타데이터는 여러 맥락에서 사용할수 있습니다.
메타 데이터의 주요한 역할.
1. 데이터 설명.
데이터의 의미, 형식, 구조를 설명해 줍니다.
2. 데이터 검색.
데이터를 쉽게 검색하고 찾을 수 있도록 도와줍니다.
예를 들어, 도서관의 카탈로그에서 책을 찾기 위해 제목, 저자, 출판 연도 등을 검색할 수 있는 것은 메타데이터 덕분입니다.
3. 데이터 관리.
데이터를 분류하고 조직화하는 데 도움을 줍니다.
4. 데이터 통제.
메타데이터를 통해 데이터의 접근 권한이나 사용 범위를 관리할 수 있습니다.
2. 메타데이터의 유형.
메타데이터는 다양한 유형으로 분류될 수 있으며, 그 목적에 따라 다르게 사용됩니다.
일반으로 많이 사용되는 메타데이터의 세 가지 유형.
1. 구조적 메타데이터(Structural Metadata)
데이터의 구조나 형식을 설명하는 정보입니다.
예를 들어, 데이터베이스에서 각 테이블의 필드와 데이터 유형을 설명하는 정보가 여기에 해당됩니다.
2. 기술적 메타데이터(Technical Metadata)
데이터를 생성하고 관리하는 데 필요한 기술적인 정보를 포함합니다.
파일의 생성 날짜, 수정 날짜, 인코딩 방식 등이 여기에 해당됩니다.
시스템에서 데이터를 처리하거나 이동시키는 데 필요한 정보가 포함됩니다.
예시: 파일 생성 일자, 수정 일자, 파일 소유자, 인코딩 방식, 해상도.
3. 설명적 메타데이터(Descriptive Metadata)
데이터를 설명하는 정보로, 데이터의 내용, 주제 키워드 등을 설명하는 데 사용됩니다.
도서관의 카탈로그나 미디어 파일의 제목, 저자, 출판 정보 등이 이에 해당됩니다.
예시: 도서의 제목, 저자, 출판일, 키워드, 요약 정보.
3. 메타데이터의 예시.
1. 웹 페이지의 메타데이터.
HTML 문서에서는 <meta> 태그를 사용하여 웹 페이지에 대한 메타데이터를 정의합니다.
이는 주로 검색 엔진이 웹 페이지를 더 잘 이해하고, 인덱싱할 수 있도록 도와줍니다.
```html
- `charset` : 문서의 문자 인코딩 방식.
- `description` : 웹 페이지의 내용에 대한 설명.
- `keyword` : 웹 페이지와 관련된 키워드.
- `author` : 문서 작성자.
- **2. 이미지 파일의 메타데이터.**
- 이미지 파일에도 메타데이터가 포함됩니다.
- 이미지 파일의 메타데이터는 카메라 설정, 촬영 위치, 해상도 등 다양한 정보를 제공할 수 있습니다.
- 이를 **EXIF(Exchangeable Image File Format)** 메타데이터라고 부릅니다.
```bash
예시 : 사진 메타데이터
- 카메라 제조사: Nikon
- 카메라 모델: D3500
- 촬영 일시: 2024-09-24
- 해상도: 6000 * 4000
- GPS 좌표: 촬영 위치 정보
3. 도서 메타데이터.
도서의 메타데이터는 도서관 카탈로그나 전자책 파일에서 찾아볼 수 있습니다.
예를 들어, 책의 제목, 저자, 출판사, ISBN 등이 메타데이터입니다.
- 제목: "1984"
- 저자: 조지 오웰
- 출판사: 하퍼콜린스
- 출판 연도: 1949
- ISBN: 978-0451524935
4. 동영상 메타데이터.
동영상 파일에도 다양한 메타데이터가 포함될 수 있습니다.
동영상의 제목, 길이, 해상도, 비트레이트, 제작 날짜 등이 여기에 해당됩니다.
- 제목: "Vaction Highlights"
- 길이: 2시간 15분
- 해상도: 1920 * 1080
- 비디오 코덱: H.264
- 파일 크기: 1.2GB
4. 메타데이터의 중요성.
1. 데이터 관리.
메타데이터는 데이터를 효율적으로 관리하고 유지하는 데 필수적입니다.
예를 들어, 검색 시능을 향상시키고, 데이터를 분류하고 정리하는 데 도움을 줍니다.
2. 데이터 검색 및 접근성.
메타데이터는 대량의 데이터를 쉽게 탐색하고 원하는 데이터를 빠르게 찾을 수 있도록 돕습니다.
검색 엔진은 웹페이지의 메타데이터를 분석하여 검색 결과를 더욱 정확하게 제공합니다.
3. 데이터 보존.
파일의 생성 일자, 수정 일자, 버전 정보 등의 메타데이터는 데이터의 이력을 추적하는 데 유용하며, 데이터를 장기적으로 보존하고 관리하는 데 도움을 줍니다.
5. 요약.
메타데이터는 데이터를 설명하는 데이터로, 파일이나 정보를 보다 쉽게 이해하고 관리할 수 있도록 도와줍니다.
메타데이터는 여러 유형으로 나뉘며, 웹페이지, 이미지, 동영상, 도서 등 다양한 형태의 데이터에 적용되어 데이터의 검색, 관리, 보존에 중요한 역할을 합니다.
3️⃣ 바이너리 데이터(Binary Data)
바이너리 데이터(Binary Data) 는 0과 1로 구성된 이진수 형태로 저장된 데이터를 말합니다.
컴퓨터 시스템은 모든 데이터를 기본적으로 이진수, 즉 바이너리 형태로 처리하기 때문에 바이너리 데이터는 컴퓨터가 이해할 수 있는 가장 기본적인 데이터 형식입니다.
텍스트 데이터는 사람이 쉽게 읽을 수 있는 형태인 반면, 바이너리 데이터는 사람이 바로 읽을 수 없는 형식으로 저장됩니다.
1. 바이너리 데이터의 특징.
1. 이진수로 표현.
바이너리 데이터는 0과 1로 구성된 이진수로 표현됩니다.
컴퓨터는 모든 데이터를 전기 신호(켜짐 = 1, 꺼짐 = 0)로 처리하기 때문에, 바이너리 데이터는 컴퓨터의 기본 데이터 표현 방식입니다.
2. 사람이 읽을 수 없는 형식.
바이너리 데이터는 사람이 직접 읽거나 해석하기 어렵습니다.
예를 들어, 이미지 파일이나 실행 파일과 같은 데이터는 일반적으로 바이너리로 저장되며, 이를 직접 열면 알아볼 수 없는 기호들이 나옵니다.
3. 파일 형식.
대부분의 파일 형식이 바이너리 형식으로 저장됩니다.
예를 들어, 이미지(JPEG, PNG), 비디오(MP4), 오디오(MP3), 실행 파일(EXE) 등은 모두 바이너리 형식으로 저장되며, 이를 사람이 읽을 수 있는 형식으로 변환하려면 특정 프로그램이나 소프트웨어가 필요합니다.
2. 바이너리 데이터의 예.
1. 이미지 파일(JPEG, PNG 등)
이미지 파일은 픽셀 값들이 0과 1로 구성된 바이너리 데이터로 저장됩니다.
이를 열고 표시하려면 이미지 뷰어와 같은 소프트웨어가 필요합니다.
2. 오디오 파일(MP3, WAV 등)
오디오 파일은 음향 신호를 디지털화한 바이너리 데이터로 저장됩니다.
음악 플레이어 프로그램을 통해 이를 재생할 수 있습니다.
3. 동영상 파일(MP4, AVI 등)
동영상 파일은 영상과 음향을 결합한 바이너리 데이터로 저장됩니다.
이를 보기 위해서는 비디오 플레이어가 필요합니다.
4. 실행 파일(EXE, DLL 등)
운영 체제에서 실행할 수 있는 프로그램은 바이너리로 저장됩니다.
사용자가 프로그램을 실행하면 컴퓨터는 이 바이너리 데이터를 해석하여 작업을 수행합니다.
5. 압축 파일(ZIP, RAR 등)
압축 파일은 여러 파일을 바이너리 형태로 압축하여 하나의 파일로 묶은 것입니다.
압축 해제 소프트웨어를 통해 원래 파일로 복원할 수 있습니다.
3. 바이너리 데이터 VS 텍스트 데이터.
텍스트 데이터
텍스트 데이터는 사람이 읽을 수 있는 형태로 저장된 데이터입니다.
주로 알파벳, 숫자, 기호 등으로 구성된 문자를 포함하며, 예를 들어 HTML, JSON, XML 파일 등이 있습니다.
텍스트 데이터는 일반 텍스트 편집기로 쉽게 열고 읽을 수 있습니다.
바이너리 데이터
바이너리 데이터는 사람이 쉽게 읽을 수 없는 이진수(0과 1)의 조합으로 저장된 데이터입니다.
이미지, 동영상, 실행 파일과 같은 데이터는 이진수로 인코딩된 바이너리 데이터 형식으로 저장됩니다.
4. 바이너리 데이터의 활용.
1. 네트워크 전송
대용량 파일(예: 이미지, 동영상, 소프트웨어 등)을 네트워크를 통해 전송할 때 바이너리 형식으로 전송됩니다.
바이너리 데이터는 압축 및 인코딩을 통해 효율적으로 전송됩니다.
2. 파일 저장
컴퓨터의 저장 장치(HDD, SSD 등)에서 모든 데이터는 바이너리 형식으로 저장됩니다.
텍스트, 이미지, 오디오, 비디오, 실행 파일 등 모든 파일은 결국 바이너리 데이터로 변환되어 저장됩니다.
3. 멀티미디어 처리
이미지, 오디오, 비디오 등의 멀티미디어 파일은 모두 바이너리 데이터로 저장되며, 이러한 데이터를 처리하기 위해서는 적절한 소프트웨어와 코덱이 필요합니다.
5. 바이너리 데이터를 처리하는 방법.
바이너리 데이터를 처리하려면 파일을 열어 이진수 데이터를 읽고 해석할 수 있는 소프트웨어나 프로그래밍 언어가 필요합니다.
예를 들어, Java, Python, C++ 등 다양한 프로그래밍 언어는 바이너리 데이터를 읽고 쓰는 기능을 제공합니다.
예시: Java에서 바이너리 파일 읽기.
import java.io.FileInputStream;
import java.io.IOException;
public class BinaryFileReader {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("image.jpg")) {
int byteData;
while ((byteData = fis.read()) != -1) {
// 1바이트씩 읽어들임
System.out.println(byteData);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
위 예시에서는 FileInputStream을 사용하여 바이너리 파일을 1바이트씩 읽는 방법을 보여줍니다.
6. 요약.
바이너리 데이터는 컴퓨터가 처리하는 기본 데이터 형식으로, 0과 1의 이진수로 구성됩니다.
주로 이미지, 오디오, 비디오, 실행 파일, 압축 파일 등에서 사용되며, 사람이 직접 읽기 어렵습니다.
텍스트 데이터와는 달리, 바이너리 데이터는 컴퓨터가 해석할 수 있는 형식으로 저장되며, 파일을 열고 처리하려면 특정 소프트웨어가 필요합니다.
4️⃣ JSON(JavaScript Object Notation)
JSON(JavaScript Object Notation) 은 데이터를 저장하고 교환하기 위한 경량 데이터 형식입니다.
사람이 읽고 쓰기 쉬우며, 기계가 분석하고 생성하기도 쉽도록 설계되었습니다.
JSON은 텍스트 형식이므로 모든 프로그래밍 언어에서 쉽게 파싱하고 생성할 수 있습니다.
1. JSON의 특징.
1. 경량 데이터 형식.
JSON은 구조가 간단하고 용량이 작어, 특히 웹 애플리케이션에서 서버와 클라이언트 간의 데이터를 주고받는 데 널리 사용됩니다.
2. 언어 독립적.
JSON은 특정 프로그래밍 언어에 의존하지 않으며, 대부분의 언어에서 JSON 데이터를 처리할 수 있는 라이브러리나 메서드를 제공합니다.
자바스크립트 문법을 기반으로 하지만, Python, Java, C#, PHP 등에서도 쉽게 사용 가능합니다.
3. 텍스트 기반.
JSON은 텍스트로 이루어져 있어 사람이 읽고 이해하기 쉽습니다.
이는 디버깅이나 데이터의 전송 및 저장에 매우 유리합니다.
2. JSON의 구조.
JSON 데이터의 기본적인 두 가지 구조.
1. 객체(Object)
중괄호 {} 로 감싸진 키-값 쌍의 집합.
키는 문자열이고, 값은 문자열, 숫자 불리언, 배열, 객체 또는 null이 될 수 있습니다.
2. 배열(Array)
대괄호 [] 로 감싸진 값들의 목록.
배열 안의 값은 순차적으로 저장되며, 각 값은 문자열, 숫자, 불리언, 객체, 배열 또는 null일 수 있습니다.
예시: JSON 객체와 배열.
{
"name": "Kobe",
"age": 30,
"isStudent": false,
"skills": ["Java", "JavaScript", "Swift"],
"address": {
"city": "Seoul",
"zipcode": "12345"
}
}
객체
이 예제에서 전체 데이터는 JSON 객체로, 중괄호 {} 안에 여러 키-값 쌍이 포함되어 있습니다.
배열
skills는 배열로, 사용자가 가진 프로그래밍 언어들을 리스트로 나타냅니다.
중첩된 객체
address는 또 다른 JSON 객체로, 중첩된 구조를 가집니다.
3. JSON의 데이터 타입.
JSON에서 사용할 수 있는 기본 데이터 타입은 다음과 같습니다.
1. 문자열(String)
큰 따옵표"로 감싸인 텍스트.
예: "Kobe"
2. 숫자(Number)
정수 또는 실수.
예: 30, 3.14
3. 불리언(Boolean)
true 또는 false
예: true, false
4. 객체(Object)
중괄호 {}로 감싸인 키-값 쌍의 집합.
예: {"name": "Kobe", "age": 30}
5. 배열(Array)
대괄호 []로 감싸인 값들의 리스트.
예: ["Java", "JavaScript", "Swift"]
6. null
값이 없음을 나타냅니다.
예: null
4. JSON의 사용 예.
1. 서버와 클라이언트 간 데이터 전송
웹 애플리케이션에서는 서버와 클라이언트 간에 JSON 형식으로 데이터를 주고받는 경우가 많습니다.
예를 들어, 사용자가 로그인할 때 서버로 전송하는 데이터는 다음과 같이 JSON 형식일 수 있습니다.
요청(Request)
{
"username": "kobe",
"password": "12345"
}
응답(Response)
{
"status": "success",
"userId": 101,
"name": "kobe"
}
2. API 응답 형식
많은 RESTful API는 JSON을 응답 형식으로 사용합니다.
예를 들어, 날씨 정보를 제공하는 API는 다음과 같은 JSON 데이터를 반환할 수 있습니다.
{
"location": "Seoul",
"temperature": 23,
"weather": "Sunny"
}
3. 설정 파일
JSON은 설정 파일 형식으로도 많이 사용됩니다.
예를 들어, JavaScript 프로젝트의 package.json 파일은 프로젝트의 설정 정보를 JSON 형식으로 저장합니다.
{
"name": "my-project",
"version": "1.0.0",
"description": "A sample project",
"dependencies": {
"express": "^4.17.1"
}
}
5. JSON 파싱과 생성.
각 프로그래밍 언어는 JSON 데이터룰 파싱하고 생성하는 방법을 지원합니다.
예를 들어, JavaScript와 Python 그리고 Java에서 JSON을 처리하는 방법은 다음과 같습니다.
JavaScript에서 JSON 처리
// JSON 문자열을 객체로 변환 (파싱)
let jsonString = '{"name": "John", "age": 30}';
let obj = JSON.parse(jsonString);
console.log(obj.name); // "John"
// 객체를 JSON 문자열로 변환
let newJsonString = JSON.stringify(obj);
console.log(newJsonString); // '{"name":"John","age":30}'
Python에서 JSON 처리
import json
# JSON 문자열을 객체로 변환 (파싱)
json_string = '{"name": "John", "age": 30}'
data = json.loads(json_string)
print(data["name"]) # "John"
# 객체를 JSON 문자열로 변환
new_json_string = json.dumps(data)
print(new_json_string) # '{"name": "John", "age": 30}'
Java에서 JSON 처리
Java에서 JSON을 처리하는 방법은 여러 가지 라이브러리를 통해 가능합니다.
가장 많이 사용되는 라이브러리로는 Jackson, Gson 그리고 JSON.simple 등 이 있습니다.
Jackson을 사용한 JSON 처리
Jackson 라이브러리는 JSON 데이터를 직렬화 및 역직렬화하는 데 강력한 기능을 제공합니다.
이를 통해 Java 객체를 JSON 형식으로 변환하거나, JSON 문자열을 Java 객체로 변환할 수 있습니다.
1. Maven 또는 Gradle에 Jackson 라이브러리 추가.
// MAVEN
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version> <!-- 사용하고자 하는 버전 -->
</dependency>
// GRADLE
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
2. Jackson을 사용하여 JSON 문자열을 Java 객체로 변환 (파싱)
import com.fasterxml.jackson.databind.ObjectMapper;
class User {
private String name;
private int age;
// 기본 생성자, getter, setter가 필요함.
public User() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class JsonExample {
public static void main(String[] args) {
String jsonString = "{\"name\":\"Kobe\", \"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
try {
// JSON 문자열을 Java 객체로 변환.
User user = objectMapper.readValue(jsonString, User.class);
System.out.println(user.getName()); // Kobe
System.out.println(user.getAge()); // 30
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. Jackson을 사용하여 Java 객체를 JSON 문자열로 변환.
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonExample {
public static void main(String[] args) {
User user = new User();
user.setName("Kobe");
user.setAge(30);
ObjectMapper objectMapper = new ObjectMapper();
try {
// Java 객체를 JSON 문자열로 변환
String jsonString = objectMapper.writeValueAsString(user);
System.out.println(jsonString); // {"name": "Kobe", "age": 30}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Gson을 사용한 JSON 처리.
Gson은 Google에서 개발한 JSON 라이브러리로, 간단하게 Java 객체를 JSON으로 직렬화하거나 JSON 문자열을 Java 객체로 역직렬화할 수 있습니다.
1. Maven 또는 Gradle에 Gson 라이브러리 추가.
// MAVEN
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version> <!-- 사용하고자 하는 버전 -->
</dependency>
// GRADLE
implementation 'com.google.code.gson:gson:2.8.8'
2. Gson을 사용하여 JSON 문자열을 Java 객체로 변환(파싱)
import com.google.gson.Gson;
public class JsonExample {
public static void main(String[] args) {
String jsonString = "{\"name\":\"Kobe\", \"age\":30}";
Gson gson = new Gson();
// JSON 문자열을 Java 객체로 변환
User user = gson.fromJson(jsonString, User.class);
System.out.println(user.getName()); // Kobe
System.out.println(user.getAge()); // 30
}
}
3. Gson을 사용하여 Java 객체를 JSON 문자열로 변환
import com.google.gson.Gson;
public class JsonExample {
public static void main(String[] args) {
User user = new User();
user.setName("Kobe");
user.setAge(30);
Gson gson = new Gson();
// Java 객체를 JSON 문자열로 변환.
String jsonString = gson.toJson(user);
System.out.println(jsonString)l // {"name": "Kobe", "age": 30}
}
}
6. JSON과 XML 비교
JSON과 XML은 모두 데이터를 구조화하는 데 사용되는 포맷입니다.
그러나 JSON은 XML에 비해 더 간결하고, 읽기 쉽고 처리 속도가 빠르다는 장점이 있어 현대 웹 애플리케이션에서 더 많이 사용됩니다.
비교 항목
JSON
XML
구조
키-값 쌍으로 이루어진 간결한 구조
태그로 둘러싸인 트리 구조
가독성
사람이 읽고 쓰기 쉬움
상대적으로 더 복잡함
데이터 크기
더 작은 크기
더 큰 크기
유연성
객체 및 배열 표현이 직관적
배열 표현이 상대적으로 복잡함
지원
대부분의 언어 및 프레임워크에서 지원
오래된 시스템과의 호환성이 좋음
7. 요약.
Java에서는 JSON 데이터를 처리하기 위해 주로 Jackson과 Gson 라이브러리를 사용합니다.
이 라이브러리들은 Java 객체와 JSON 간의 변환을 간단하고 효율적으로 할 수 있게 해줍니다.
ObjectMapper(Jackson) 또는 Gson 객체를 사용하여 JSON 데이터를 Java 객체로 변환하거나, 반대로 JSON으로 변환할 수 있습니다.
JSON은 데이터를 저장하고 교환하는 데 사용되는 경량 텍스트 형식입니다.
구조는 객체와 배열로 구성되며, 다양한 데이터 타입(문자열, 숫자, 객체, 배열 등)을 지원합니다.
웹 애플리케이션에서 서버와 클라이언트 간의 데이터 전송에 널리 사용됩니다.
대부분의 프로그래밍 언어에서 쉽게 파싱하고 생성할 수 있는 라이브러리를 제공합니다.
간결한 구조 덕분에 XML보다 많이 사용되며, 특히 RESTful API에서 주로 사용됩니다.
5️⃣ @PostMapping 애너테이션.
@PostMapping 애너테이션 은 Spring Framework에서 HTTP POST 요청을 처리하기 위해 사용하는 애너테이션입니다.
주로 클라이언트가 서버로 데이터를 전송할 때 사용됩니다.
예를 들어, 폼 데이터나 JSON 데이터를 서버로 제출하여 새로운 리소스를 생성하는 경우에 많이 사용됩니다.
1. @PostMapping의 역할.
HTTP POST 요청 처리
@PostMapping은 POST 요청을 특정 URL에 매핑하여 처리합니다.
주로 데이터 생성(create) 작업에서 사용되며, 서버로부터 데이터를 전송할 때 사용됩니다.
RESTful API에서 데이터 생성
RESTful API에서 리소스를 생성하는 작업을 처리할 때 POST 요청이 사용됩니다.
예를 들어, 사용자를 생성하거나 데이터베이스에 새로운 항목을 추가하는 등의 작업이 있을 수 있습니다.
기본 사용법.
@RestController
public class UserController {
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// 사용자 생성 로직 처리
return userService.saveUser(user); // 생성된 사용자 객체 반환
}
}
2. 주요 특징.
1. 요청 데이터 전송
@PostMapping은 주로 요청 본문(Body)에 데이터를 포함하여 전송합니다.
이는 GET 요청과 달리 URL에 데이터를 담지 않고, HTTP 요청의 Body에 데이터를 담아 서버로 전송합니다.
2. @RequestBody와 함께 사용
서버로 전달되는 JSON 또는 XML 데이터를 Java 객체로 변환하려면 @RequestBody 애너테이션과 함께 사용합니다.
@RequestBody는 요청 본문에 포함된 데이터를 Java 객체로 매핑하는 역할을 합니다.
3. 폼 데이터 처리
HTML 폼을 통해 데이터를 전송할 때도 @PostMapping을 사용하여 폼 데이터를 처리할 수 있습니다.
이때는 @ModelAttribute 또는 @RequestParam을 사용하여 폼 필드를 바인딩합니다.
3. 예시 1: JSON 데이터를 POST 요청으로 처리.
@RestController
public class UserController {
// POST 요청을 처리하며, 요청 본문을 Java 객체로 변환
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// user 객체는 클라이언트가 보낸 JSON 데이터로부터 매핑됨
System.out.println("User created: " + user.getName());
return userService.saveUser(user); // 새로운 사용자 객체 반환
}
}
@PostMapping("/users)": /users URL로 들어오는 POST 요청을 처리합니다.
@RequestBody: 요청 본문에 포함된 JSON 데이터를 User 객체로 변환합니다.
클라이언트는 아래와 같은 JSON 데이터를 서버로 보낼 수 있습니다.
{
"name": "Kobe",
"age": 30
}
4. 예시 2: HTML 폼 데이터 처리.
@Controller
public class UserController {
// HTML 폼에서 제출된 데이터를 처리
@PostMapping("/register")
public String registerUser(@RequestParam String name, @RequestParam int age) {
// 폼 데이터 처리 로직
System.out.println("User name: " + name + ", age: " + age);
return "user_registered"; // 성공 페이지로 이동
}
}
@RequestParam: 폼 필드에서 제출된 데이터를 메서드 파라미터로 바인딩합니다.
클라이언트는 HTML 폼을 통해 데이터를 전송할 수 있습니다.
<form action="/register" method="post">
<input type="text" name="name" placeholder="Enter your name">
<input type="number" name="age" placeholder="Enter your age">
<button type="submit">Register</button>
</form>
5. @PostMapping 과 @RequestMapping 비교.
@PostMapping은 @RequestMapping(method = RequestMethod.POST)를 간단하게 대체할 수 있는 방법입니다.
@RequestMapping(value = "/users", method = RequestMethod.POST)
public User createUser(@RequestBody User user) {
return userService.saveUser(user);
}
위 코드를 @PostMapping으로 리팩토링.
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.saveUser(user);
}
6. 요약.
@PostMapping은 HTTP POST 요청을 처리하기 위한 애너테이션입니다.
주로 새로운 리소스를 생성하거나 서버로 데이터를 전송할 때 사용됩니다.
JSON 데이터를 처리할 때는 @RequestBody와 함께 사용되며, 폼 데이터는 @RequestParam 또는 @ModelAttribute와 함께 처리할 수 있습니다.
@PostMapping은 @RequestMapping의 간결한 대안으로 사용됩니다.
6️⃣ @RequestBody 애너테이션.
@RequestBody 애너테이션 은 Spring Framework에서 HTTP 요청의 본문(바디, Body) 에 담긴 데이터를 Java 객체로 변환해주는 역할을 합니다.
주로 POST, PUT과 같은 요청에서 클라이언트가 JSON, XML 또는 다른 형식의 데이터를 전송할 때 이를 서버에서 처리할 수 있도록 도와줍니다.
1. @RequestBody의 주요 역할.
1. 요청 본문(Request Body)을 Java 객체로 변환
@RequestBody는 클라이언트가 요청 본문에 담아 보낸 데이터를 Java 객체로 변환합니다.
이때, Spring은 주로 Jackson 라이브러리를 사용하여 JSON 데이터를 Java 객체로 변환합니다.
2. 주로 POST, PUT 요청에서 사용
@RequestBody는 POST는 PUT 요청에서 데이터를 서버로 전송할 때 많이 사용됩니다.
예를 들어, 클라이언트가 새로운 리소스를 생성하거나 데이터를 업데이트할 때 JSON 데이터를 본문에 담아 전송할 수 있습니다.
3. 자동 역직렬화
클라이언트가 JSON 형식으로 데이터를 보내면, Spring은 이를 자동으로 Java 객체로 변환(역직렬화) 해줍니다.
2. RequestBody의 사용법.
예시 1: JSON 데이터를 Java 객체로 변환.
클라이언트가 서버로 JSON 데이터를 전송하고, 서버가 이를 Java 객체로 변환하여 처리하는 예시입니다.
@RestController
public class UserController {
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// 요청 본문에서 전송된 JSON 데이터를 User 객체로 변환
System.out.println("User name: " + user.getName());
System.out.println("User age: " + user.getAge());
// User 객체를 저장하거나 처리한 후 반환
return userService.saveUser(user);
}
}
@PostMApping("/users"): /users 경로로 들어오는 POST 요청을 처리합니다.
@RequestBody User user: 요청 본문에 있는 JSON 데이터를 User 객체로 변환합니다.
클라이언트가 전송하는 JSON 데이터 예시
{
"name": "Kobe",
"age": 30
}
위 JSON 데이터를 서버로 보내면, @RequestBody가 이를 User 객체로 변환합니다.
User 클래스
public class User {
private String name;
private int age;
// 기본 생성자, getter, setter
public User() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
3. 예시 2: 요청 본문에 포함된 JSON 데이터 사용.
@RestController
public class ProductController {
@PutMapping("/products/{id}")
public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
// 요청 본문에서 Product 객체로 변환된 데이터를 사용해 업데이트 처리
product.setId(id);
return productService.updateProduct(product);
}
}
@RequestBody: JSON 데이터를 Java 객체인 Product로 변환합니다.
@PathVariable: URL 경로에 포함된 값을 메서드 파라미터로 사용합니다. 위 코드에서는 제품 ID를 경로에서 추출합니다.
4. 요청 본문과의 관계.
HTTP 요청에서 요청 본문은 실제로 전송되는 데이터가 포함된 부분입니다.
클라이언트가 서버로 데이터를 보내는 방식.
1. 요청 URL에 쿼리 파라미터로 데이터를 포함(GET 요청 등에서 사용)
2. 요청 본문에 데이터를 포함(POST, PUT 요청에서 사용)
@RequestBody는 두 번째 경우인 요청 본문에 데이터를 담아 보내는 요청에서 사용됩니다.
5. 데이터 형식.
@RequestBody는 JSON, XML 등 다양한 형식의 데이터를 처리할 수 있습니다.
기본적으로 Spring은 Jackson을 사용하여 JSON 데이터를 처리하지만, XML 등의 다른 형식도 지원됩니다.
JSON
대부분의 경우 JSON 형식의 데이터가 요청 본문에 담겨 전송되며, Spring은 이를 자동으로 Java 객체로 변환합니다.
XML
필요에 따라 XML 데이터를 사용할 수도 있으며, Spring에서 XML을 처리하기 위한 라이브러리를 추가하면 XML도 처리 가능합니다.
6. 추가 속성.
required 속성
@RequestBody 는 기본적으로 요청 본문에 데이터가 반드시 포함되어야 합니다.
하지만, required = false로 설정하면 요청 본문이 없어도 예외를 발생시키지 않습니다.
@PostMapping("/users")
public User createUser(@RequestBody(required = false) User user) {
if (user == null) {
// 본문이 없을 경우 처리 로직
return new User("Anonymous", 0);
}
return userService.saveUser(user);
}
7. 요약.
@RequestBody 는 클라이언트가 보낸 HTTP 요청의 본문을 Java 객체로 변환하는 데 사용됩니다.
주로 POST, PUT 요청에서 JSON, XML 등의 데이터를 서버로 전송할 때 사용됩니다.
Spring은 기본적으로 Jackson 라이브러리를 사용하여 JSON 데이터를 Java 객체로 변환합니다.
요청 본문에 포함된 데이터를 쉽게 처리할 수 있도록 도와주며, RESTful API 개발에 자주 사용됩니다.
-
🍃[Spring] Controller, DTO, API 명세, `@GetMapping`, MIME, `@RequestParam`, 폼 데이터, `@RestController`
🍃[Spring] Controller, DTO, API 명세, @GetMapping, MIME, @RequestParam, 폼 데이터, @RestController
1️⃣ Controller.
Controller 는 웹 애플리케이션의 요청을 처리하는 핵심 구성 요소로, 사용자 요청(URL 요청)을 받아 이를 처리한 후 적절한 응답(뷰 또는 데이터를 반환)을 제공하는 역할을 합니다.
이는 보통 MVC(Model-View-Controller) 패턴에서 Controller 에 해당하며, 클라이언트의 입력을 받아 비즈니스 로직과 상호작용하고 결과를 반환하는 흐름을 담당합니다.
Controller는 주로 @Controller 또는 @RestController 애너테이션을 사용하여 정의 됩니다.
1. @Controller 와 @RestController 애너테이션의 차이점.
1. @Controller
일반적으로 뷰(View) 를 반환합니다.
Thymeleaf와 같은 템플릿 엔진을 사용할 때 HTML 파일을 반환할 때 사용됩니다.
예: 사용자 요청을 처리하고 HTML 페이지를 렌더링하는 경우.
2. @RestController
주로 JSON, XML 등 데이터 포맷으로 응답을 반환합니다.
@Controller 와 @ResponseBody 를 함께 사용하는 것과 동일한 역할을 합니다.
RESTful API를 구현할 때 주로 사용됩니다.
2. 기본 예시.
1. @Controller 를 이용한 HTML 페이지 반환
@Controller
public class HomeController {
@GetMapping("/home")
public String homePage(Model model) {
model.attribute("message", "Welcome to the home page!");
return "home"; // resource/templates/home.html로 연결됨(Thymeleaf와 같은 템플릿 엔진 사용)
}
}
2. @RestController 를 이용한 JSON 응답 반환
@RestController
public class ApiController {
@GetMapping("/api/data")
public ResponseEntity<String> getData() {
return new ResponseEntity<>("Hello, this is JSON data!", HttpStatus.OK);
}
}
3. Controller의 주요 역할.
요청 매핑
URL을 특정 메서드에 매핑하여 적절한 처리를 하도록 합니다.(@GetMapping, @PostMapping, @RequestMapping 등 사용)
비즈니스 로직 호출
서비스 계층과 상호작용하여 필요한 작업을 수행합니다.
모델과 뷰 처리
데이터를 처리한 후 뷰로 데이터를 전달하거나 JSON 등의 형식으로 응답합니다.
Spring Boot의 Controller는 이러한 방식으로 애플리케이션의 핵심 흐름을 제어하며, 웹 요청을 처리하는 중요한 역할을 담당합니다.
2️⃣ DTO(Data Transfer Object)
DTO(Data Transfer Object) 는 계층 간의 데이터를 전송할 때 사용하는 객체입니다.
일반적으로, 백엔드 애플리케이션에서는 여러 계층(Controller, Service, Repository 등) 간에 데이터를 주고받게 되는데, 이때 사용자가 전달한 데이터를 담거나 가공된 데이터를 전달하기 위해 DTO를 사용합니다.
1. DTO의 주요 목적.
1. 데이터 캡슐화
DTO는 특정 데이터 구조를 캡슐화하여 외부에 노출됩니다.
이로 인해 불필요한 데이터 노출을 방지할 수 있습니다.
2. 성능 최적화
대규모 데이터를 한 번에 전송하는 것보다는, 필요한 데이터만 DTO를 통해 전송하는 것이 네트워크 트래픽 및 성능을 최적화하는 데 도움이 됩니다.
3. 계층 분리
DTO는 주로 서비스 계층에서 데이터를 조작하고, 이를 컨트롤러 계층으로 전달하는 데 사용됩니다.
이를 통해 애플리케이션의 각 계층이 명확하게 분리되며, 유지보수성과 확장성이 형성됩니다.
2. DTO와 엔티티의 차이점.
엔티티(Entity)
엔티티는 데이터베이스 테이블과 1:1로 매핑되며, 주로 영속성(Persistence)을 담당합니다.
데이터베이스와의 상호작용을 관리하는 역할을 하며, 비즈니스 로직을 포함하지 않는 것이 일반적입니다.
DTO
DTO는 데이터를 전달하는 데 중점을 두며, 보통 엔티티를 변환하거나 특정 로직을 처리하기 위해 사용됩니다.
엔티티와는 다르게 데이터베이스와 직접적으로 매핑되지 않으며, 전송하고자 하는 데이터만을 포함합니다.
3. 사용 예시.
다음은 간단한 User 엔티티와 UserDTO의 예시입니다.
User 엔티티
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// getters and setters
}
UserDTO
public class UserDTO {
private String name;
private String email;
// 생성자, getters, setters
}
이처럼 엔티티는 데이터베이스와 연결된 객체이고, DTO는 사용자에게 데이터를 전달하거나 요청을 받아올 때 사용되는 객체입니다.
3️⃣ API 명세(API Specification)
API 명세(API Specification) 는 API(Application Programming Interface)의 동작 방식, 요청 및 응답 형식, 사용 가능한 메서드, 파라미터 등을 명확히 설명한 문서입니다.
API 명세는 API 사용자(주로 개발자)들이 API를 정확히 이해하고 사용할 수 있도록 도와주는 중요한 문서입니다.
1. API 명세의 주요 내용.
1. API 개요.
API가 어떤 목적을 가지고 있는지, 주로 어떤 기능을 수행하는지 간단한 설명을 포함합니다.
2. 엔드포인트(Endpoint)
API의 접근 경로를 의미하며, 각 엔드포인트는 특정 기능을 수행합니다.
예를 들어 GET /users는 사용자의 목록을 가져오는 API 엔드포인트일 수 있습니다.
3. HTTP 메서드
각 엔드포인트에 사용할 수 있는 HTTP 메서드(GET, POST, PUT, DELETE 등)가 명시됩니다.
예를 들어, GET /users는 사용자 목록을 조회하고, POST /users는 새 사용자를 생성하는 API를 의미할 수 있습니다.
4. 요청(Request) 파라미터
API 요청 시 포함할 수 있는 파라미터나 데이터 형식을 명시합니다.
파라미터는 경로 변수(Path Variables), 쿼리 파라미터(Query Parameters), 헤더(Headers), 또는 바디(Body)에 포함될 수 있습니다.
경로 변수 : GET /users/{id}에서 {id}가 경로 변수 입니다.
쿼리 파라미터 : GET /users?status=active에서 status가 쿼리 파라미터입니다.
5. 요청 본문(Request Body)
주로 POST나 PUT 같은 요청에서 사용되며, JSON, XML 또는 폼 데이터 등으로 전송할 데이터를 정의합니다.
예를 들어, 사용자를 생성하는 API에서는 다음과 같은 JSON 요청이 본문이 있을 수 있습니다.
{
"name": "Jhon Doe",
"email": "jhon@example.com"
}
6. 응답(Response)
API 요청에 대한 응답 형식을 설명합니다.
성공적인 요청이나 실패했을 때의 응답 코드(예: 200 OK, 404 Not Found)와 함께 응답 본문(Response Body) 형식도 명시됩니다.
{
"id": 1,
"name": "Jhon Doe",
"email" "jhon@example.com"
}
7. 상태 코드(Status Code)
각 응답의 결과를 나타내는 HTTP 상태 코드를 포함합니다.
일반적인 상태 코드는 다음과 같습니다.
200 OK : 성공적인 요청.
201 Created : 리소스 생성 성공.
400 Bad Request : 잘못된 요청.
401 Unauthorized : 인증 실패.
404 Not Found : 리소스를 찾을 수 없음
500 Internet Server Error : 서버에서 발생한 오류
8. 예시(Examples)
실제로 요청을 보내는 방법과 그에 대한 응답을 보여주는 예시가 포함될 수 있습니다.
예를 들어, curl 명령어를 사용한 HTTP 요청 방법 등을 명시합니다.
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john.doe@example.com"}'
2. API 명세의 중요성.
개발자 간의 의사소통
API 명세는 API를 제공하는 측과 사용하는 측 간의 원활한 의사소통을 가능하게 합니다.
명세가 정확할수록 API를 사용할 때 발생할 수 있는 혼란이나 오류가 줄어듭니다.
일관성
모든 엔드포인트와 요청/응답 형식이 일관되게 설계되고 구현될 수 있도록 도와줍니다.
유지보수
명세를 기준으로 API를 변경하거나 확장할 때, 이를 참고하여 기존 사용자들에게 영향을 최소화하면서 시스템을 개선할 수 있습니다.
3. API 명세 도구.
API 명세는 다양한 형식으로 작성될 수 있으며, 대표적인 도구와 표준은 다음과 같습니다.
OpenAPI(Swagger)
가장 널리 사용되는 API 명세 표준으로, Swagger UI를 통해 시각화와 테스트 기능을 제공합니다.
Postman
API 테스트 및 명세 작성을 위한 도구로, 다양한 HTTP 메서드를 테스트하고 명세를 자동으로 생성할 수 있습니다.
RAML
RESTful API Modeling LAnguage의 약자로, REST API 명세 작성을 위해 사용됩니다.
API 명세는 API를 사용하려는 모든 개발자에게 필수적인 가이드 역할을 하며, 정확하고 명확하게 작성하는 것이 중요합니다.
4️⃣ @GetMapping 애너테이션.
@GetMapping 애너테이션 은 Spring Framework에서 HTTP GET 요청을 처리하기 위해 사용하는 애너테이션입니다.
주로 Spring MVC에서 컨트롤러의 메서드에 붙여서 특정 URL로 들어오는 GET 요청을 처리할 수 있도록 매핑합니다.
1. @GetMapping의 기본 동작.
Spring Boot 애플리케이션에서는 클라이언트가 서버로 GET 요청을 보낼 때 해당 요청을 처리할 컨트롤러 메서드를 지정하기 위해 @GetMapping을 사용합니다.
이 애너테이션은 내부적으로 @RequestMapping(method = RequestMethod.GET)과 동일한 역할을 합니다.
2. 사용 예시.
다음은 @GetMapping을 사용하여 특정 URL에서 GET 요청을 처리하는 간단한 예시입니다.
@RestController
public class UserController {
// '/users' 경로에 대한 GET 요청 처리
@GetMapping("/users")
public List<User> getAllUsers() {
// 서비스 계층에서 사용자 목록을 가져와 반환하는 메서드
return userService.getAllUsers();
}
}
위의 코드에서
@GetMapping("/users")는 /users 경로로 들어오는 GET 요청을 처리합니다.
메서드 getAllUsers()는 GET 요청을 처리하며, 보통 클라이언트에게 데이터를 반환합니다.
이때 반환하는 데이터는 기본적으로 JSON 형식으로 반환됩니다(Spring Boot에서는 @RestController 사용 시).
3. 주요 특징.
1. HTTP GET 요청 처리.
@GetMapping은 GET 요청을 처리하는 데만 사용되며, 다른 HTTP 메서드(POST, PUT, DELETE 등) 요청은 처리하지 않습니다.
2. 경로 지정.
@GetMapping("/path") 형식으로 매핑 경로를 지정할 수 있습니다.
경로에 고정된 URL뿐만 아니라 경로 변수, 쿼리 파라미터 등도 포함될 수 있습니다.
3. 경로 변수 사용.
경로 변수(Path Variable)를 사용하여 동적으로 URL을 처리할 수도 있습니다.
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
위 코드에서 @PathVariable을 사용하여 URL 경로에 포함된 id 값을 메서드 파라미터로 전달받습니다.
예를 들어, /users/1이 요청되면 id가 1로 설정됩니다.
4. 쿼리 파라미터 사용.
쿼리 파라미터(Query Parameter)를 처리할 때도 사용할 수 있습니다.
@GetMapping("/users")
public List<User> getUsersByStatus(@RequestParam String status) {
return userService.getUsersByStatus(status);
}
위 예시에서 @RequestParam을 사용하여 쿼리 파라미터 status를 메서드 파라미터로 전달받습니다.
예를 들어, /users?status=active로 요청하면 status 값은 “active”가 됩니다.
4. @GetMapping 과 관련된 주요 옵션.
produces
응답으로 제공하는 데이터의 MIME 타입을 지정할 수 있습니다.
예를 들어, JSON, XML 형식의 응답을 지정할 수 있습니다.
@GetMapping(value = "/users", produces = "application/json")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
params
특정 쿼리 파라미터가 존재하는 경우에만 해당 메서드가 매핑되도록 제한할 수 있습니다.
@GetMapping(value = "/users", params = "active")
public List<User> getActiveUsers() {
return userService.getActiveUsers();
}
위 예시에서 active라는 쿼리 파라미터가 있어야만 이 메서드가 호출됩니다.
5. @GetMapping VS @RequestMapping
@RequestMapping 을 사용하면 HTTP 메서드를 지정해야 합니다.
// @RequestMapping
@RequestMapping(value = "/users", method = RequestMethod.GET)
public List<User> getAllUsers() {
return userService.getAllUsers();
}
// @GetMapping
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping은 GET 요청 전용으로 보다 간결하게 작성할 수 있는 방법입니다.
리팩토링 포인트
1. @RequestMapping 은 다양한 HTTP 메서드(GET, POST, PUT, DELETE 등)를 처리할 수 있도록 설계된 범용 애너테이션입니다. 그러나 GET 요청만을 처리할 때는 @GetMapping을 사용하여 코드를 간결하게 만들 수 있습니다.
2. method = RequestMethod.GET 대신 @GetMapping을 사용함으로써 더 직관적이고 간단하게 GET 요청을 처리할 수 있습니다.
3. @GetMapping은 기본적으로 GET 요청을 처리하므로 value 속성에 경로만 명시하면 됩니다.
6. 요약.
주요 역할
HTTP GET 요청을 특정 메서드에 매핑하여 요청 처리.
간결한 문법
@RequestMapping의 GET 요청 처리를 간결하게 대체.
주로 사용되는 곳
리소스 조회, 데이터 가져오기 등의 역할을 수행할 때 사용.
5️⃣ MIME(Mutipurpose Internet Mail Extensions) 타입.
MIME(Mutipurpose Internet Mail Extensions) 타입 은 인터넷에서 전송되는 데이터의 형식을 나타내는 표준입니다.
MIME 타입은 클라이언트와 서버 간에 주고받는 데이터가 어떤 형식인지 정의하여, 이를 통해 클라이언트(브라우저 등)는 데이터의 처리를 어떻게 해야 헐지 알 수 있습니다.
1. MIME 타입의 구성.
MIME 타입은 크게 두 부분으로 나뉩니다.
타입(Type)
데이터의 일반적인 범주를 나타냅니다.(예: text, image, application 등).
서브타입(Subtype)
데이터의 구체적인 형식을 나타냅니다.(예: html, plain, json, jpeg 등).
형식은 /(슬래시)로 구분되며, 예를 들어 text/html은 HTML 문서라는 의미입니다.
2. MIME 타입의 예시.
1. 텍스트 형식.
text/plain
일반 텍스트(ASCII 또는 UTF-8로 인코딩된 텍스트).
text/html
HTML 문서.
text/css
CSS 파일.
text/javascript
JavaScript 파일.
2. 이미지 형식.
image/jpeg
JPEG 이미지.
image/png
PNG 이미지.
image/gif
GIF 이미지.
3. 응용 프로그램 형식.
application/json
JSON 형식의 데이터.
application/xml
XML 문서.
appliccation/pdf
PDF 파일.
application/octet-stream
일반적인 바이너리 데이터.
파일 다운로드 시 주로 사용됩니다.
4. 멀티미디어 형식.
audio/mpeg
MP3 오디오 파일.
video/mp4
MP4 비디오 파일.
5. 기타 형식.
multipart/form-data
주로 파일 업로드나 폼 데이터를 전송할 때 사용되는 형식.
3. MIME 타입의 역할.
MIME 타입은 웹 브라우저와 같은 클라이언트가 서버로부터 받은 데이터의 형식을 이해하고 적절하게 처리하는 데 중요한 역할을 합니다.
예를 들어
text/html
브라우저는 이 MIME 타입을 받으면 이를 HTML 문서로 해석하고 화면에 렌더링합니다.
application/json
브라우저는 이 MIME 타입을 받으면 이를 JSON 형식의 데이터로 인식하고, 보통 개발자 도구에서 구조화된 형식으로 보여줍니다.
application/octet=stream
바이너리 파일(예: 프로그램 파일, 압축 파일)을 다운로드할 때 사용되며, 클라이언트는 이 데이터를 파일로 저장할 수 있습니다.
4. MIME 타입 지정.
서버가 클라이언트에 데이터를 보낼 때 MIME 타입을 지정하는데, HTTP 응답 헤더의 Content-Type 필드를 통해 MIME 타입이 정의됩니다.
예시: Content-Type 헤더
HTTP/1.1 200OK
Content=Type: application/json
{
"name": "John",
"age": 30
}
위 예시에서는 서버가 JSON 형식의 데이터를 응답하며, Content-Type: application/json 을 통해 MIME 타입을 명시하고 있습니다.
5. Spring에서의 MIME 타입 지정.
Spring에서는 @GetMapping 또는 @PostMaping 과 같은 애너테이션을 사용할 때 produces 속성을 통해 MIME 타입을 지정할 수 있습니다.
예시 : JSON 형식의 응답 지정.
@GetMapping(value = "/users", produces = "application/json")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
위 코드에서는 /users로 들어오는 GET 요청에 대해 JSON 형식 (application/json)으로 데이터를 응답하도록 지정하고 있습니다.
6. 요약.
MIME 타입 은 인터넷에서 전송되는 데이터의 형식을 나타내는 표준.
주로 클라이언트와 서버 간에 데이터를 주고받을 때 데이터 형식을 지정하여 클라이언트가 이를 적절히 처리할 수 있도록 도와줌
형식 은 타입/서브타입 으로 구성되며, 예를 들어 text/html 은 HTML 문서를 나타냄.
Spring과 같은 프레임워크에서도 응답 형식에 따라 적절한 MIME 타입을 지정할 수 있음.
6️⃣ @RequestParam 애너테이션.
@RequestParam 애너테이션 은 Spring MVC에서 HTTP 요청의 쿼리 파라미터, 폼 데이터 또는 URL에 포함된 파라미터를 메서드 파라미터에 바인딩하는 데 사용됩니다.
주로 GET 요청에서 뭐리 스트링의 파라미터 값을 가져오거나, POST 요청에서 폼 데이터로 전달된 파라미터 값을 처리하는 데 사용됩니다.
1. 기본 사용법.
@RequestParam 을 사용하면 클라이언트가 요청한 URL의 파라미터 값을 메서드의 인자로 받을 수 있습니다.
예를 들어, 사용자가 GET /users?name=Jhon 과 같이 요청하면 name 파라미터 값을 메서드에서 받을 수 있습니다.
예시 1: 단일 쿼리 파라미터 받기.
@GetMapping("/users")
public String getUserByName(@RequestParam String name) {
return "Requested user: " + name;
}
위 코드에서
@RequestParam String name
쿼리 파라미터 name의 값을 받아서 메서드 파라미터로 전달합니다.
예를 들어 /users?name=Jhon으로 요청하면 name에 "John" 값이 들어갑니다.
예시 2: 여러 쿼리 파라미터 받기.
@GetMapping("/users")
public String getUser(@RequestParam String name, @RequestParam int age) {
return "User: " + name + ", Age: " + age;
}
이 코드는 두 갸의 쿼리 파라미터(name과 age)를 받습니다.
클라이언트가 /users?name=Jhon&age=25로 요청하면 name은 "Jhon", age는 25가 됩니다.
3. 선택적 파라미터와 기본값.
@RequestParam의 속성을 이용하면 선택적인 파라미터를 정의할 수 있습니다.
파라미터가 없을 경우 기본값을 설정할 수 있습니다.
예시 3: 기본값 설정.
@GetMapping("/users")
pulbic String getUser(@RequestParam(defaultValue = "Unknown") String name) {
return "Requested user: " + name;
}
위 코드에서 클라이언트가 /users로만 요청을 보내면, name의 기본값으로 "Unknown"이 설정됩니다.
/users?name=John으로 요청하면 name은 "John" 값이 됩니다.
예시 4: 필수 여부 설정.
required 속성을 사용하면 파라미터가 필수인지 선택적인지 설정할 수 있습니다.
기본적으로 @RequestParam은 필수입니다.
하지만, required = false로 설정하면 파라미터가 없어도 예외를 발생시키지 않습니다.
@GetMapping("/users")
public String getUser(@RequestParam(required = false) String name) {
return "Requested user: " + (name != null ? name : "Guest");
}
이 경우, 클라이언트가 name 파라미터를 포함하지 않고 요청할 경우 name 값은 null이 되고, Guest로 대체 될 수 있습니다.
4. 배열 또는 리스트 파라미터 처리.
@RequestParam을 사용하여 여러 값을 한 번에 받을 수도 있습니다.
예시 5: 배열로 파라미터 받기.
@GetMapping("/users")
public String getUsersByNames(@RequestParam List<String> names) {
return "Requested users: " + String.join(", ", names);
}
위 코드에서 /users?names=John&names=Jane과 같이 요청하면 names는 ["John", "Jane"] 리스트로 전달됩니다.
5. 요약.
@RequestParam은 URL 쿼리 파라미터나 폼 데이터를 컨트롤러 메서드의 파라미터로 매핑하는 데 사용됩니다.
필수 여부(required), 기본값(defaultValue) 설정을 통해 유연하게 파라미터를 처리할 수 있습니다.
여러 파라미터나 배열/리스트 형태의 파라미터도 받을 수 있습니다.
7️⃣ 폼 데이터(Form Data).
폼 데이터(Form Data) 는 웹 애플리케이션에서 사용자가 웹 브라우저를 통해 서버로 제출하는 데이터를 말합니다.
주로 HTML <form> 요소를 사용하여 사용자 입력을 서버로 전송하며, 사용자가 입력한 텍스트, 파일, 선택한 옵션 등의 다양한 데이터를 포함할 수 있습니다.
1. 폼 데이터의 구성.
폼 데이터는 여러 종류의 입력 필드를 통해 생성되며, 전송 방식에 따라 다르게 처리됩니다.
HTML <form> 태그를 이용하여 입력 폼을 만들고, 사용자가 입력한 데이터를 서버로 전송할 수 있습니다.
HTML 폼의 예시.
<form action="/submit" method="post">
<label for="name">Name:</label>
<input type="text" id="name" name="name">
<label for="age">Age:</label>
<input type="number" id="age" name="age">
<label for="file">Profile Picture:</label>
<input type="file" id="file" name="profilePicture">
<button type="submit">Submit</button>
</form>
이 폼에서는 다음과 같은 데이터를 서버로 전송할 수 있습니다.
Name : 텍스트 입력.
Age : 숫자 입력.
Profile Picture : 파일 업로드.
2. 폼 데이터의 전송 방식.
1. GET 메서드
폼이 GET 메서드를 사용하여 데이터를 전송할 때는 데이터가 URL의 쿼리 스트링(Query String)으로 전달 됩니다.
URL은 보통 다음과 같은 형식을 가집니다.
example.com/submit?name=Jhon&age=30
데이터는 URL에 포함되기 떄문에 보안에 취약하고, 전송 가능한 데이터의 양이 제한적입니다.
따라서 검색 쿼리나 필터와 같은 간단한 데이터를 전송할 때 주로 사용됩니다.
<form action="/submit" method="get">
<!-- 입력 필드 -->
<button type="submit">Submit</button>
</form>
2. POST 메서드
POST 메서드를 사용하면 데이터가 HTTP 요청 본문(Body)에 포함되어 전송됩니다.
이 방식은 URL에 데이터를 노출하지 않기 때문에 더 안전하며, 대용량 데이터를 전송할 수 있습니다.
파일 업로드나 민감한 정보를 전송할 때 주로 사용됩니다.
<form action="/submit" method="post">
<!-- 입력 필드 -->
<button type="submit">Submit</button>
</form>
3. 전송되는 데이터 형식.
폼 데이터가 전송될 때, 브라우저는 Content-Type 헤더를 통해 서버에 어떤 방식으로 데이터를 인코딩했는지 알려줍니다.
폼 데이터의 인코딩 방식에 따라 서버에서 데이터를 처리하는 방식이 달라집니다.
1. application/x-www.form-urlencoded
기본 폼 인코딩 방식입니다.
폼 데이터를 URL 인코딩하여 키-값 쌍으로 전송합니다.
예를 들어, name=John&age=30처럼 키와 값을 &로 구분하여 서버로 전송합니다.
POST 요청의 본문에 이 형식으로 데이터가 포함됩니다.
예시
name=Jhon&age=30
2. multipart/form-data
파일 업로드가 포함된 폼 데이터를 전송할 때 사용되는 인코딩 방식입니다.
데이터와 파일을 여러 부분으로 나누어 서버로 전송합니다.
파일뿐만 아니라 텍스트 필드도 함께 전송할 수 있습니다.
예시: 다음과 같은 방식으로 파일과 데이터를 함께 전송합니다.
```plaintext
–boundary
Content-Disposition: form-data; name=”name”
John
–boundary
Content-Disposition: form-data; name=”profilePicture”; filename=”profile.jpg”
Content-Type: image/jpeg
[바이너리 파일 데이터]
–boundary–
```
3. text/plain
데이터를 단순한 텍스트 형식으로 전송합니다.
주로 API와의 간단한 텍스트 전송에 사용되며, 폼 데이터로는 잘 사용되지 않습니다.
4. 서버에서 폼 데이터 처리.
서버는 클라이언트가 전송한 폼 데이터를 처리합니다.
예를 들어, Spring Boot에서는 @RequestParam 또는 @ModelAttribute 등을 사용하여 폼 데이터를 쉽게 처리할 수 있습니다.
예시: Spring에서 폼 데이터 받기.
@PostMapping("/submit")
public String handleForm(@RequestParam String name, @RequestParam int age) {
// 폼 데이터 처리
return "Name: " + name + ", Age: " + age;
}
서버는 클라이언트가 보낸 폼 데이터에서 name과 age 값을 추출하여 처리합니다.
5. 요약.
폼 데이터는 사용자가 웹 브라우저의 폼을 통해 서버로 전송하는 데이터를 의미합니다.
폼 데이터는 GET 또는 POST 메서드를 통해 전송되며, URL 인코딩 방식이나 multipart/form-data 방식으로 서버에 전달됩니다.
서버는 해당 데이터를 받아 처리하여 사용자 요청에 맞는 결과를 반환합니다.
8️⃣ @RestController 애너테이션.
@RestController 애너테이션 은 Spring Framework에서 RESTful 웹 서비스의 컨트롤러를 정의할 때 사용하는 애너테이션입니다.
이 애너테이션은 컨트롤러 클래스에 붙이며, 이 클래스가 JSON이나 XML과 같은 데이터를 HTTP 응답 본문으로 직접 반환하도록 해줍니다.
1. @RestController의 특징.
1. @Controller + @ResponseBody의 결합.
@RestController는 내부적으로 @Controller와 @ResponseBody 애너테이션을 결합한 것입니다.
@Controller는 Spring MVC에서 View를 반환하는 전통적인 컨트롤러를 정의하는 데 사용되지만, @RestController는 View를 반환하지 않고, 객체나 데이터를 직접 HTTP 응답 본문으로 반환합니다.
@ResponseBody는 메서드가 반환하는 객체를 JSON이나 XML로 직렬화하여 HTTP 응답의 본문으로 반환하도록 하는 역할을 합니다.
@RestController는 클래스 레벨에서 모든 메서드에 @ResponseBody가 자동으로 적용됩니다.
2. RESTful 웹 서비스에 적합.
@RestController는 주로 RESTful 웹 서비스 개발에 사용되며, 클라이언트가 서버로부터 JSON, XML 등의 데이터를 받을 수 있도록 설계되었습니다.
전통적인 MVC 패턴에서는 데이터를 뷰 페이지(HTML 등)로 전달하지만, RESTful 웹 서비스에서는 데이터 자체(JSON, XML 등)를 응답으로 전달합니다.
2. 예시 코드.
@RestController
public class UserController {
@GetMapping("/users")
public List<User> getAllUsers() {
// 사용자 목록을 반환하는 예시
return userService.getAllUsers();
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// 새로운 사용자를 생성하는 예시
return userService.saveUser(user);
}
}
@RestController
이 클래스는 RESTful 컨트롤러이며, 모든 메서드는 JSON 또는 XML과 같은 데이터를 HTTP 응답 본문으로 반환합니다.
@GetMapping("/users")
GET 요청을 처리하며, List<User> 객체를 JSON 형식으로 반환합니다.
@PostMapping("/users")
POST 요청을 처리하며, 클라이언트가 보낸 User 데이터를 받아 새로운 사용자를 생성하고 그 결과를 JSON 형식으로 반환합니다.
3. @RestController와 일반 컨트롤러(@Controller)의 차이.
1. 주된 목적.
@RestController는 데이터(주로 JSON, XML)를 직접 반환하는 데 사용되며, API 서버를 구축할 때 주로 사용됩니다.
@Controller는 뷰(HTML, JSP 페이지 등)를 반환하는 데 주로 사용되며, 웹 애플리케이션의 화면을 보여줄 때 적합합니다.
2. View 해석.
@RestController는 데이터를 응답 본문에 바로 전달하며, JSP나 Thymeleaf 같은 뷰 템플릿 엔진을 사용하지 않습니다.
@Controller는 뷰 이름을 반환하고, Spring MVC는 이 뷰 이름을 해석하여 JSP나 Thymeleaf 같은 템플릿 엔진을 사용해 화면을 렌더링합니다.
3. @ResponseBody 필요 여부.
@RestController를 사용하면 메서드마다 @ResponseBody를 붙일 필요가 없습니다.
클래스 레벨에서 자동으로 적용됩니다.
@Controller를 사용할 경우, 데이터를 반환하려면 메서드마다 @ResponseBody를 붙여서 응답 본문에 데이터를 보내도록 명시해야 합니다.
일반 컨트롤러(@Controller) 예시
@Controller
public class ViewController {
@GetMapping("/home")
public String home() {
// "home.html"이라는 뷰를 반환함
return "home";
}
}
이 코드는 “home”이라는 뷰 이름을 반환하며, Spring은 home.html 또는 home.jsp를 찾아서 렌더링하게 됩니다.
4. 요약.
@RestController는 RESTful 웹 서비스 개발에 사용되며, 데이터를 JSON이나 XML 등의 형식으로 반환합니다.
@RestController는 @Controller와 @ResponseBody의 결합으로, 데이터를 응답 본문에 바로 반환하도록 설계되었습니다.
Spring MVC에서 웹 애플리케이션의 뷰를 렌더링하는 @Controller 와 달리, @RestController는 API 개발에 더 적합합니다.
-
🌐[Network] 네트워크, 포트, 도메인 이름, IP, DNS
🌐[Network] 네트워크, 포트, 도메인 이름, IP, DNS.
1️⃣ 네트워크란 무엇인가요?
네트워크(Network) 는 여러 대의 컴퓨터나 장치들이 서로 연결되어 데이터를 주고받는 시스템을 말합니다.
네트워크는 이러한 장치들 간에 정보를 교환하고 자원을 공유하는 것을 가능하게 합니다.
인터넷을 포함한 모든 디지털 통신 환경에서 네트워크는 필수적인 요소이며, 네트워크의 규모나 종류에 따라 다양한 방식으로 구현됩니다.
1. 네트워크의 주요 개념.
1. 장치(노드, Node).
네트워크에 연결된 컴퓨터, 스마트폰, 서버, 라우터 등 각 장치는 “노드”라고 부르며, 이들은 서로 데이터를 주고받습니다.
2. 통신 매체.
네트워크에서 장치들이 데이터를 주고받기 위해 사용하는 물리적 또는 무선 매체입니다.
대표적인 예로는 유선 네트워크와 무선 네트워크가 있습니다.
유선 네트워크.
이더넷 케이블, 광섬유 케이블 등을 통해 데이터가 전송됩니다.
무선 네트워크.
와이파이, 블루투스, 5G와 같은 무선 통신 기술을 통해 데이터를 주고받습니다.
3. 프로토콜.
네트워크에서 통신하기 위한 규약이나 규칙을 말합니다.
TCP/IP(Transmission Control Protocol / Internet Protocol) 가 대표적인 프로토콜로, 인터넷에서 데이터를 주고 받는데 사용됩니다.
4. 네트워크 주소.
각 장치는 네트워크 상에서 고유한 주소(IP 주소)를 가지고 있으며, 이를 통해 데이터를 정확한 대상에게 전송할 수 있습니다.
2. 네트워크의 유형.
1. LAN(Local Area Network, 근거리 통신망)
작은 범위에서 사용되는 네트워크로, 학교, 회사, 가정 내에서 장치들을 연결할 때 사용됩니다.
빠른 속도와 낮은 지연 시간으로 장치들 간에 자원을 쉽게 공유할 수 있습니다.
예: 가정에서 사용되는 와이파이 네트워크, 회사의 내부 네트워크 등.
2. WAN(Wide Area Network, 광역 통신망)
넓은 지리적 범위를 아우르는 네트워크로, 도시, 국가, 전 세계에 걸쳐 여러 LAN을 연결합니다.
대표적인 예로 인터넷이 있으며, 전 세계의 컴퓨터와 장치를 연결합니다.
3. MAN(Metropolitan Area Network, 도시권 통신망)
도시 또는 특정 지역 내에서 여러 LAN을 연결하여 형성된 네트워크입니다.
LAN보다 범위는 넓지만 WAN보다는 좁은 범위를 커버합니다.
도시 내 기업 본사의 여러 지점을 연결하는 경우가 이에 해당합니다.
4. PAN(Personal Area Network, 개인 통신망)
개인 장치들 간의 네트워크로, 주로 블루투스 또는 USB로 연결된 스마트폰, 태블릿, 노트북 등의 장치들이 여기에 속합니다.
3. 네트워크의 역할.
1. 데이터 공유.
네트워크를 통해 파일, 이미지, 문서 등을 서로 다른 장치로 전송하고, 사용자들이 여러 자원을 함께 사용할 수 있습니다.
예: 네트워크 프린터를 통해 여러 컴퓨터에서 한 대의 프린터를 사용할 수 있습니다.
2. 자원 공유.
네트워크를 통해 서버, 프린터, 스토리지와 같은 하드웨어 자원을 여러 사용자나 장치가 공동으로 사용할 수 있습니다.
3. 애플리케이션 지원.
웹 브라우저, 이메일 클라이언트, 클라우드 서비스와 같은 애플리케이션 네트워크를 통해 통신하고 데이터를 주고 받습니다.
4. 네트워크의 구성 요소.
1. 라우터(Router)
네트워크 상에서 데이터를 전달하는 장치로, 여러 네트워크 간의 트래픽을 관리하고 최적의 경로를 찾아 데이터를 전달합니다.
2. 스위치(Switch)
네트워크 내 장치들이 데이터를 주고받을 수 있도록 연결해주는 장치로, 주로 LAN에서 사용됩니다.
3. 방화벽(Firewall)
네트워크 보안을 위해 외부의 불법적인 접근을 차단하고, 네트워크 내부의 데이터를 보호하는 장치입니다.
4. 서버(Server)
네트워크에서 데이터를 제공하거나 처리하는 장치로, 웹 페이지 제공, 데이터 저장, 애플리케이션 실행 등을 담당합니다.
5. 네트워크 프로토콜.
1. TCP/IP
인터넷과 대부분의 네트워크에서 사용되는 핵심 프로토콜로, 데이터를 패킷으로 나누어 전달하고 목적지에서 재조합하는 방식입니다.
2. HTTP/HTTPS
웹 브라우저와 서버 간에 데이터를 주고받는 프로토콜입니다.
HTTP는 SSL/TLS 암호화를 통해 보안을 강화한 버전입니다.
3. FTP
파일 전송 프로토콜로, 네트워크 상에서 파일을 업로드하거나 다운로드하는 데 사용됩니다.
4. DNS
도메인 이름을 IP 주소로 변환하는 시스템으로, 사람이 기억하기 쉬운 도메인 이름을 통해 해당 IP 주소의 서버에 접근할 수 있게 해줍니다.
6. 네트워크의 보안.
네트워크는 외부 공격이나 악의적인 사용자로부터 보호하기 위해 다양한 보안 장치와 소프트웨어를 사용합니다.
네트워크 보안에는 암호화, 인증, 방화벽 설정, 침입 탐지 시스템(IDS) 등 이 포함됩니다.
7. 요약.
네트워크는 여러 컴퓨터나 장치들이 상호 연결되어 데이터를 주고받을 수 있도록 하는 시스템입니다.
네트워크는 장치 간의 데이터 및 자원 공유를 가능하게 하며, LAN, WAN, PAN 등의 다양한 유형으로 구현될 수 있습니다.
인터넷 역시 세계 최대읜 네트워크이며, 네트워크는 우리 일상 생활과 비즈니스 환경에서 필수적인 요소로 자리 잡고 있습니다.
2️⃣ Port란 무엇인가요?
네트워크에서 포트(port) 는 컴퓨터나 네트워크 장치에서 특정한 프로세스나 서비스를 구분하기 위해 사용되는 가상의 논리적 통신 경로입니다.
네트워크 상에서 하나의 장치가 여러 서비스를 동시에 제공할 수 있기 때문에, 포트 번호 는 동일한 IP 주소 내에서 서로 다른 서비스들을 구분하는 중요한 역할을 합니다.
1. 포트의 주요 개념.
1. 포트 번호.
포트는 숫자로 구분되며, 일반적으로 0번부터 65535번까지의 범위를 가집니다.
포트 번호는 통신하는 애플리케이션이나 프로세스를 구별하는 데 사용됩니다.
0번 ~ 1023번 : 잘 알려진 포트(well-known ports) 로, 주요 프로토콜이나 서비스가 이 범위의 포트를 사용합니다.
예시
HTTP(웹) : 포트 80
HTTPS(보안 웹) : 포트 443
FTP(파일 전송) : 포트 21
SMTP(이메일 전송) : 포트 25
1024번 ~ 49151번 : 등록된 포트(registered ports) 로, 특정 애플리케이션이 예약하여 사용하는 포트입니다.
49152번 ~ 65535번 : 동적 포트 / 사설 포트(dynamic or private ports) 로, 임시 연결이나 사용자 정의 애플리케이션에서 사용합니다.
2. 포트의 역할.
네트워크 상에서 클라이언트가 서버에 접속할 때, IP 주소를 통해 서버의 위치를 식별하고, 포트 번호를 통해 해당 서버에서 동작하는 특정 서비스(프로세스)를 구분합니다.
예를 들어, 웹 브라우저가 example.com에 접속할 때, 기본적으로 HTTP 요청을 위해 포트 80을 사용하거나 HTTPS 요청을 위해 포트 443을 사용합니다.
2. 포트의 사용 예시.
클라이언트-서버 통신.
서버는 특정 포트에서 대기 상태(listening)로 클라이언트의 요청을 기다립니다.
예를 들어, 웹 서버는 보통 포트 80(HTTP) 또는 443(HTTPS)에서 요청을 수신합니다.
클라이언트는 서버의 IP 주소와 함께 특정 포트로 요청을 보냅니다.
이때 클라이언트는 임시 포트를 사용하여 서버와의 통신 경로를 설정합니다.
동시 실행 서비스 구분.
동일한 서버에서 여러 서비스가 동작하는 경우, 각각의 서비스는 다른 포트를 통해 통신을 처리합니다.
예를 들어, 한 서버에서 웹 서버가 포트 80을 사용하면서 동시에 데이터베이스 서버는 포트 3306(MySQL)에서 실행될 수 있습니다.
3. 포트 번호를 통한 통신 예시.
웹 브라우저와 웹 서버 간의 통신.
사용자가 웹 브라우저에 http://example.com을 입력하면, 브라우저는 DNS 서버를 통해 example.com의 IP 주소를 확인합니다.
브라우저는 그 IP 주소로 포트 80(HTTP)을 통해 요청을 보냅니다.
웹 서버는 포트 80에서 클라이언트의 요청을 수신하고, 처리 후 응답을 보냅니다.
이때, 클라이언트는 자신의 컴퓨터에서 임의의 동적 포트(예: 50000번대)를 사용하여 통신을 시작합니다.
4. 방화벽과 포트.
네트워크 보안에서는 방화벽(firewall) 이 중요한 역할을 하며, 포트 기반으로 네트워크 트래픽을 제어합니다.
방화벽은 허용된 포트에서만 트래픽을 수신하거나 송신하도록 설정할 수 있습니다.
예를 들어, 웹 서버는 80번과 443번 포트만 열어 두고, 나머지 포트는 차단하여 보안을 강화할 수 있습니다.
5. 요약
포트는 네트워크 상에서 동일한 IP 주소를 사용하는 여러 서비스나 애플리케이션을 구분하기 위한 숫자 기반의 논리적 경로입니다.
포트 번호는 클라이언트와 서버 간의 통신에서 서비스를 식별하고, 각 포트는 특정 애플리케이션이 네트워크 트래픽을 처리할 수 있게 해줍니다.
특정 포트는 표준 서비스에 예약되어 있으며, 네트워크 보안에서 방화벽 등을 통해 포트를 관리하고 보호할 수 있습니다.
3️⃣ 도메인 이름(domain name)이란 무엇인가요?
도메인 이름(domain name)은 인터넷에서 웹사이트나 특정 네트워크 자원을 식별하기 위해 사용되는 고유한 텍스트 기반의 주소입니다.
도메인 이름은 사람이 읽을 수 있는 형태로, IP 주소와 연결되어 있습니다.
실제로 인터넷에서 모든 장치와 서버는 숫자로 된 IP 주소로 통신하지만, 사람이 기억하고 사용하기 편리하도록 도메인 이름을 사용합니다.
1. 도메인 이름의 구성.
도메인 이름은 여러 부분으로 구성되며, 각 부분은 점(.)으로 구분됩니다.
일반적인 도메인 이름의 구성요소.
최상위 도메인(TLD, Top-Level Domain)
도메인의 가장 마지막 부분으로, 주로 .com, .org, .net, .gov 등의 일반적인 TLD 또는 국가 코드 도메인(.kr, .jp, .uk 등)으로 끝납니다.
예: example.com 에서 .com이 TLD 입니다.
두 번째 레벨 도메인(SLD, Second-Level Domain)
TLD 바로 앞에 위치하며, 주로 회사나 단체의 이름 또는 웹사이트의 이름이 들어갑니다.
예: example.com 에서 example이 SLD입니다.
서브도메인
때때로 SLD 앞에 추가되는 부분으로, 주로 특정 하위 섹션이나 서비스를 나타냅니다.
예: blog.example.com에서 blog가 서브 도메인입니다.
따라서, 도메인 이름은 다음과 같은 구조를 가집니다.
서브 도메인.두 번째 레벨 도메인.최상위 도메인
예: www.example.com에서
www는 서브도메인(생략 가능)
example은 두 번째 레벨 도메인
com은 최상위 도메인
2. 도메인 이름의 역할.
1. IP 주소 대체.
도메인 이름은 사람이 읽기 쉽고 기억하기 쉽도록 IP 주소(예: 192.198.1.1)를 대신합니다.
DNS(Domain Name System) 서버는 도메인 이름을 해당 IP 주소로 변환하는 역할을 하여 웹 브라우저가 올바른 서버에 연결되도록 합니다.
2. 식별 및 브랜드.
도메인 이름은 특정 웹사이트나 서비스의 정체성을 나타내며, 회사나 단체, 개인의 온라인 존재를 대표합니다.
예를 들어, google.com은 구글의 웹사이트를 식별하는 데 사용됩니다.
3. 웹 주소(URL).
도메인 이름은 웹 주소(URL)의 중요한 부분을 구성합니다.
URL은 도메인 이름과 더불어 특정 페이지나 리소스를 가리킵니다.
예: https://www.example.com/about에서 www.example.com이 도메인 이름이고, /about은 특정 페이지 경로입니다.
3. 도메인 이름의 등록.
도메인 이름을 소유하려면 도메인 등록 기관을 통해 원하는 도메인을 등록해야 합니다.
등록된 도메인은 일정 기간동안(주로 1년 단위) 사용 가능하며, 이후 갱신해야 합니다.
도메인 이름은 전 세계적으로 고유해야 하므로, 이미 등록된 도메인은 다른 사람이 사용할 수 없습니다.
4. 요약.
도메인 이름은 IP 주소를 대신해 인터넷 상의 리소스를 쉽게 식별하고 접근할 수 있도록 만들어진 텍스트 기반의 주소입니다.
이를 통해 사람들은 기억하기 쉬운 이름을 사용하여 웹사이트에 접근할 수 있고, 도메인 이름은 네트워크 자원의 온라인 정체성을 나타내는 중요한 역할을 합니다.
4️⃣ IP란 무엇인가요?
IP(Internet Protocol)는 인터넷이나 네트워크 상에서 데이터를 주고 받기 위한 규약(프로토콜)입니다.
IP는 네트워크 상에서 장치들이 서로를 식별하고 통신할 수 있도록 하는 주소 체계와 데이터 전송 방식을 정의합니다.
즉, IP는 인터넷 통신의 기본이 되는 프로토콜로, 모든 인터넷 연결 장치는 고유한 IP 주소를 가지고 있으며, 이 주소를 통해 데이터를 주고 받습니다.
1. IP 주소.
IP 주소(Internet Protocol Address) 는 네트워크에 연결된 각 장치(컴퓨터, 스마트폰, 서버 등)를 식별하기 위해 할당된 고유한 숫자입니다.
IP 주소는 컴퓨터 네트워크 상에서 데이터를 송수신할 때 발신자와 수신자의 주소 역할을 합니다.
IPv4 와 IPv6 두 가지 버전이 있습니다.
IPv4
32비트 주소 체계를 사용하여 약 43억 개의 주소를 제공합니다.
IPv4 주소는 보통 4개의 10진수로 표현되며, 예를 들어 “192.168.0.1”과 같은 형태를 띱니다.
IPv6
128비트 주소 체계를 사용하여 훨씬 더 많은 주소를 제공하여, 점점 더 많은 장치가 인터넷에 연결됨에 따라 IPv6가 필요하게 되었습니다.
IPv6 주소는 16진수로 표현되며, 예를 들어”2001:0db8:85a:0000:0000:8a2e:0370:7334” 같은 형태를 가집니다.
2. IP의 기능.
IP는 데이터를 네트워크 상에서 한 장치에서 다른 장치로 전송하는 데 필요한 방법과 경로를 결정합니다.
이 과정은 여러 단계를 거쳐 이루어지며, IP는 각 데이터를 작은 패킷으로 나누어 전달하는 역할을 합니다.
패킷 분할
데이터를 작은 패킷으로 분할하여 전송합니다.
이 패킷들은 각기 다른 경로를 통해 목적지로 전달될 수 있으며, 도착 후 다시 조립되어 원래의 데이터로 복원됩니다.
주소 지정
IP는 발신자와 수신자의 IP 주소를 포함하여 패킷이 어느 위치에서 출발하고 어느 위치로 가야 하는지 명확히 합니다.
라우팅
IP 패킷은 여러 네트워크 장비(라우터 등)를 통해 목적지로 전달됩니다.
IP 프로토콜은 이 과정에서 패킷이 최적의 경로로 이동할 수 있도록 라우팅합니다.
3. IP의 역할.
데이터 전달.
IP는 네트워크 상에서 데이터를 전달하는 가장 기본적인 프로토콜입니다.
이는 웹 페이지 로드, 이메일 송수신, 파일 전송 등 거의 모든 네트워크 작업에서 사용됩니다.
네트워크 계층의 역할.
IP는 OSI(Open System Interconnection) 모델의 네트워크 계층(3계층)에서 작동하며, 데이터를 패킷으로 분할하고 이를 전송할 때 사용되는 주소 체계와 경로 결정 방식을 담당합니다.
무신뢰성.
IP 자체는 “무신뢰성”이 있는 프로토콜입니다.
즉, 데이터를 전송할 때 손실이 발생할 수 있으며, 데이터를 정확히 전달했는지 확인하지 않습니다.
이러한 신뢰성 보장은 TCP(Transmission Control Protocol)와 같은 상위 계층 프로토콜에서 처리됩니다.
4. IP와 TCP.
TCP/IP
IP는 일반적으로 TCP와 함께 사용되며, 이 두 프로토콜을 묶어서 TCP/IP 프로토콜 스택이라고 부릅니다.
TCP는 데이터의 신뢰성을 보장하고, IP는 데이터를 목적지로 전송하는 역할을 합니다.
TCP는 데이터를 손실 없이 정확히 전송했는지 확인하고, 필요한 경우 다시 요청하여 데이터의 신뢰성을 유지합니다.
5. 요약.
IP(Internet Protocol)는 네트워크 상에서 데이터를 전송하기 위한 규약으로, IP 주소를 사용하여 장치를 식별하고 데이터를 전송합니다.
IP는 인터넷 통신의 기본이 되는 중요한 요소로, 패킷을 통해 데이터를 전달하고 이를 최적의 경로로 라우팅하는 역할을 합니다.
5️⃣ DNS.
DNS(Domain Name System) 는 사람이 읽을 수 있는 도메인 이름(예: www.example.com)을 컴퓨터가 이해할 수 있는 IP 주소(예: 192.198.1.1 또는 IPv6 주소)로 변환하는 시스템입니다.
DNS는 인터넷에서 도메인 이름을 기반으로 웹 사이트나 서비스에 접근할 수 있게 하는 핵심 인프라입니다.
1. DNS의 주요 역할.
인터넷 상의 모든 컴퓨터는 서로 통신하기 위해 IP 주소를 사용합니다.
그러나 IP 주소는 사람이 기억하기 어렵기 때문에 도메인 이름을 사용하여 웹 사이트나 서비스를 쉽게 접근할 수 있도록 하는 것이 DNS의 역할입니다.
DNS는 다음과 같은 주요 기능을 수행합니다.
1. 도메인 이름을 IP 주소로 변환
사용자가 웹 브라우저에 도메인 이름을 입력하면, DNS는 해당 도메인 이름에 매핑된 IP 주소를 반환하여 사용자가 방문하려는 서버와 통신할 수 있게 합니다.
예: 사용자가 www.google.com을 입력하면, DNS 서버는 이 도메인에 해당하는 IP 주소를 찾아 브라우저에 전달합니다.
2. 분산형 데이터베이스
DNS는 전 세계에 걸쳐 분산된 데이터베이스 시스템으로 구성되어 있으며, 도메인 이름과 IP 주소 매핑 정보를 효율적으로 관리하고 제공합니다.
2. DNS의 동작 과정.
DNS는 웹 사이트에 접속할 때 자동으로 작동하는 백그라운드 프로세스입니다. 그 과정은 다음과 같습니다.
1. 도메인 이름 입력
사용자가 웹 브라우저에 도메인 이름(예: www.example.com)을 입력합니다.
2. 로컬 DNS 확인
사용자의 컴퓨터는 먼저 로컬 캐시(컴퓨터에 저장된 최근의 DNS 조회 기록) 를 확인하여 해당 도메인 이름의 IP 주소가 저장되어 있는지 확인합니다.
만약 로컬 캐시에 정보가 없다면, 컴퓨터는 설정된 DNS 서버 로 요청을 보냅니다.
3. DNS 서버 요청
사용자의 컴퓨터는 ISP(인터넷 서비스 제공자)에서 제공한 DNS 서버 에 도메인 이름의 IP 주소를 요청합니다.
4. DNS 서버의 재귀적 조회
루트 DNS 서버
도메인 이름이 처음 입력되면, DNS 서버는 루트 DNS 서버에 도메인의 최상위 도메인(TLD, 예: .com)에 대한 정보를 요청합니다.
TLD 서버
루트 서버는 TLD 서버(예: .com 도메인을 관리하는 서버)의 주소를 반환하고, DNS 서버는 해당 TLD 서버에 요청을 보냅니다.
권한 있는 DNS 서버
TLD 서버는 해당 도메인의 권한 있는 DNS 서버(예: example.com에 대한 DNS 서버)의 IP 주소를 반환합니다.
권한 있는 DNS 서버는 최종적으로 해당 도메인의 IP 주소를 DNS 서버에 전달합니다.
5. IP 주소 반환
DNS 서버는 IP 주소를 받아 사용자에게 반환합니다.
그런 다음 브라우저는 이 IP 주소로 해당 서버에 연결하여 웹 페이지를 로드합니다.
6. 캐시 저장
이 과정에서 조회된 IP 주소는 캐시에 저장되어 이후 동일한 도메인에 대한 요청이 있을 때 더 빠르게 처리됩니다.
3. DNS의 주요 구성 요소.
1. DNS 클라이언트
사용자의 컴퓨터나 스마트폰과 같은 장치에서 DNS 요청을 발생시키는 프로그램입니다.
주로 웹 브라우저가 이러한 역할을 수행합니다.
2. DNS 서버
도메인 이름과 IP 주소 매핑 정보를 저장하고 관리하는 서버입니다.
다양한 유형의 DNS 서버가 존재합니다.
재귀 DNS 서버
사용자의 요청을 받아 필요한 정보를 찾아주는 서버입니다.
ISP에서 제공하는 기본 DNS 서버입니다.
권한 있는 DNS 서버(Authoritative DNS Server)
특정 도메인에 대한 최종적인 정보를 제공하는 서버로, 그 도메인의 IP 주소를 관리합니다.
루트 DNS 서버
인터넷의 최상위 DNS 서버로, 도메인 이름의 최상위 도메인(TLD)에 대한 정보를 제공합니다.
3. DNS 레코드
DNS 서버에 저장된 도메인과 관련된 정보입니다.
DNS 레코드는 여러 유형이 있으며, 각각 고유한 목적을 가집니다.
A 레코드
도메인 이름을 IPv4 주소로 매핑합니다.
AAAA 레코드
도메인 이름을 IPv6 주소로 매핑합니다.
CNAME 레코드
하나의 도메인 이름을 다른 도메인 이름에 매핑합니다.
MX 레코드
이메일 전송을 위해 메일 서버의 도메인 이름을 정의합니다.
4. DNS 캐싱.
DNS는 성능을 향상시키기 위해 캐싱을 사용합니다.
캐싱은 DNS 서버나 사용자 장치에 이전에 조회한 DNS 정보를 일정 시간 동안 저장하여, 동일한 요청이 있을 때 더 빠르게 처리할 수 있도록 합니다.
캐시된 정보는 TTL(Time To Live)이라는 제한 시간 동안 유효하며, 이 시간이 지나면 새로운 정보를 조회합니다.
5. DNS의 중요성.
1. 인터넷 사용의 편리성
DNS 덕분에 우리는 복잡한 숫자로 된 IP 주소를 외울 필요 없이, 도메인 이름을 통해 웹 사이트에 접속할 수 있습니다.
2. 네트워크 효율성
DNS는 트래픽을 효율적으로 관리하며, 각 요청을 빠르게 처리할 수 있도록 분산된 시스템으로 구성되어 있습니다.
3. 보안
DNS는 네트워크 바안에서도 중요한 역할을 합니다.
그러나 DNS 요청이 가로채기(예: DNS 스푸핑)에 취약할 수 있어, 이를 방지하기 위해 DNSSEC(DNS Security Extension) 와 같은 보안 기술이 도입되었습니다.
6. 요약.
DNS는 인터넷의 전화번호부와 같은 역할을 하며, 도메인 이름을 IP 주소로 변환하여 사용자가 쉽게 웹 사이트나 서비스를 사용할 수 있게 해줍니다.
DNS는 분산된 시스템으로 동작하며, 여러 DNS 서버들이 협력하여 사용자의 요청을 처리합니다.
이를 통해 인터넷 사용자들은 간편하게 네트워크 자원에 접근할 수 있습니다.
-
🌐[Network] HTTP, API, URL, 쿼리, 바디
🌐[Network] HTTP, API, URL, 쿼리, 바디
1️⃣ HTTP란 무엇인가요?
HTTP(Hypertext Transfer Protocol) 는 WWW(World Wide Web)에서 정보를 주고받기 위한 기본적인 프로토콜입니다.
웹 브라우저와 웹 서버 간의 통신을 가능하게 하는 규약으로, 클라이언트(Client, 사용자의 웹 브라우저)가 서버(Server)에 요청(Request)을 보내고, 서버가 이에 대한 응답(Response)을 보내는 방식으로 동작합니다.
1. HTTP의 주요 특징.
1. 텍스트 기반 프로토콜.
HTTP는 사람이 읽을 수 있는 텍스트 형식으로 요청과 응답을 주고받습니다.
요청 메시지에는 클라이언트가 서버에 요구하는 작업이, 응답 메시지에는 서버가 요청에 대해 처리한 결과가 포함됩니다.
2. 비연결성.
HTTP는 비연결성 프로토콜입니다.
즉, 클라이언트와 서버 간에 요청을 처리할 때만 연결을 맺고, 응답이 완료되면 연결을 끊습니다.
이후 요청이 있을 때마다 새로운 연결을 맺습니다.
이를 통해 자원 사용을 줄일 수 있지만, 지속적인 연결이 필요한 애플리케이션에서는 한계가 있습니다.
3. 무상태성.
HTTP는 상태를 유지하지 않는 프로토콜입니다.
즉, 서버는 클라이언트의 요청을 각각 독립적으로 처리하며, 이전 요청의 상태나 정보를 기억하지 않습니다.
이를 해결하기 위해 쿠키(cookie), 세션(session), JWT(Json Web Token)등을 사용하여 상태 정보를 유지합니다.
4. 포트 번호.
HTTP는 기본적으로 80번 포트 를 사용하며, 보안이 강화된 HTTPS는 443번 포트 를 사용합니다.
2. HTTP의 동작 방식.
HTTP는 클라이언트-서버 구조로 동작하며, 주로 웹 브라우저가 클라이언트 역할을 하고, 웹 서버가 서버 역할을 합니다.
기본적인 요청과 응답 과정은 다음과 같습니다.
1. 클라이언트 요청.
사용자가 웹 브라우저에 http://example.com과 같은 URL을 입력하면, 클라이언트(웹 브라우저)가 서버에 HTTP 요청을 보냅니다.
이 요청은 웹 페이지나 이미지 파일과 같은 자원을 요구하는 내용입니다.
2. 서버 응답.
서버는 클라이언트의 요청을 처리한 후, 요청한 자원(예: HTML 문서)을 HTTP 응답으로 돌려줍니다.
3. 웹 페이지 표시.
클라이언트는 받은 응답을 해석하여 웹 페이지를 렌더링하거나 자원을 표시합니다.
3. HTTP 요청 메시지 구조.
HTTP 요청 메시지는 다음과 같은 구조를 가집니다.
1. 요청 라인(Request Line)
메서드(Method) : 클라이언트가 서버에 요청하는 작업을 나타냅니다.
GET : 서버로부터 데이터를 요청.
POST : 서버에 데이터를 전송.
PUT : 서버에 데이터를 업데이트.
DELETE : 서버에서 데이터를 삭제.
URL : 요청하려는 자원의 위치를 나타냅니다.
HTTP 버전 : 사용 중인 HTTP 프로토콜의 버전 정보입니다.(예: HTTP/1.1)
예: GET /index.html HTTP/1.1
2. 헤더(Header)
요청에 대한 추가 정보를 제공합니다.
예를 들어, User-Agent 헤더는 요청을 보내는 클라이언트 정보를 담고 있으며, Accept 헤더는 클라이언트가 수락할 수 있는 데이터 형식을 지정합니다.
3. 본문(Body)
POST 나 PUT과 같은 메서드를 사용할 때 클라이언트가 서버로 전송할 데이터를 포함합니다.
GET 요청에서는 본문이 없는 경우가 대부분입니다.
4. HTTP 응답 메시지 구조.
1. 상태 라인(Status Line)
HTTP 버전
서버가 사용하는 HTTP 버전입니다.
상태 코드(Status Code)
요청 처리 결과를 나타내는 숫자 코드입니다.
상태 설명(Status Text)
상태 코드에 대한 설명입니다.
예: HTTP/1.1 200 OK
2. 헤더(Header)
응답에 대한 부가 정보를 제공합니다.
예를 들어, Content-Type 헤더는 응답 본문의 데이터 형식을 지정하며, Content-Length는 응답 데이터의 길이를 나타냅니다.
3. 본문(Body)
서버가 클라이언트에게 보내는 실제 데이터가 포함됩니다.
예를 들어, HTML 문서, 이미지, JSON 데이터 등이 본문에 담길 수 있습니다.
5. HTTP 상태 코드.
HTTP 응답에서 상태 코드는 클라이언트가 보낸 요청의 처리 결과를 나타냅니다. 주요 상태 코드는 다음과 같습니다.
1xx (정보) : 요청이 수신되어 처리 중입을 나타냅니다.
2xx (성공) : 요청이 성공적으로 처리되었음을 나타냅니다.
200 OK : 요청이 성공적으로 처리되었음.
3xx (리다이렉션) : 요청한 자원이 영구적으로 다른 위치로 이동했음을 나타냅니다.
301 Moved Permanently : 요청한 자원이 영구적으로 다른 위치로 이동함.
302 Found : 요청한 자원이 일시적으로 다른 위치로 이동함.
4xx(클라이언트 오류) : 클라이언트의 요청에 오류가 있음을 나타냅니다.
400 Bad Request : 잘못된 요청.
404 Not Found : 요청한 자원이 존재하지 않음.
5xx(서버 오류) : 서버에서 요청을 처리하는 중에 오류가 발생했음을 나타냅니다.
500 Internal Server Error : 서버에서 발생한 일반적인 오류.
503 Service Unavailable : 서버가 현재 요청을 처리할 수 없음.
6. HTTP의 한계와 HTTPS
HTTP는 기본적으로 암호화되지 않은 프로토콜 입니다.
따라서, 데이터가 중간에 가로채기(스니핑) 당할 수 있으며, 민감한 정보가 보호되지 않습니다.
이를 해결하기 위해 HTTPS(Hypertext Transfer Protocol Secure) 가 도입되었습니다.
HTTPS는 HTTP에 SSL/TLS 암호화 계층 을 추가하여 데이터를 안전하게 주고받을 수 있도록 합니다.
이를 통해 웹 사이트와 사용자 간의 통신이 보호됩니다.
7. 요약.
HTTP는 웹에서 클라이언트(주로 웹 브라우저)와 서버 간에 데이터를 주고받기 위한 프로토콜로, 요청(Request)-응답(Response) 구조로 동작하며 텍스트 기반의 메시지를 주고받습니다.
비연결성과 무상태성을 가지며, 상태 코드와 헤더를 통해 요청 및 응답에 대한 정보를 제공하여 웹 상에서의 통신을 지원합니다.
HTTPS는 HTTP의 보안 강화를 위해 도입된 버전입니다.
2️⃣ API(Application Programming Interface)
API(Application Programming Interface)는 응용 프로그램 간에 상호작용할 수 있도록 정의된 규칙과 도구의 모음입니다.
즉, API는 소프트웨어 애플리케이션이 서로 통신하고 데이터를 주고받기 위한 인터페이스를 제공합니다.
API를 사용하면 한 프로그램이 다른 프로그램의 기능을 활용하거나 데이터를 가져올 수 있게 됩니다.
1. API의 주요 개념.
1. 인터페이스.
API는 두 소프트웨어 간에 중개 역할을 하여, 서로 다른 시스템이나 애플리케이션이 일정한 규칙 에 따라 데이터를 교환할 수 있게 해줍니다.
이를 통해 각 시스템이 상호작용하면서도 독립성을 유지할 수 있습니다.
2. 표준화된 규칙.
API는 개발자들이 특정 기능을 쉽게 사용할 수 있도록 표준화된 방식 으로 제공됩니다.
예를 들어, 웹 API는 HTTP 요청을 통해 데이터를 전송하거나 받는 규칙을 따릅니다.
3. 추상화.
API는 내부 구현 방식을 숨기고, 외부에서는 단순한 인터페이스만 제공하여 복잡한 작업을 쉽게 수행할 수 있도록 합니다.
예를 들어, Google Maps API를 사용하면 복잡한 지도 데이터 처리 과정을 몰라도 간단한 명령을 통해 지도 정보를 활용할 수 있습니다.
2. API의 유형.
1. 웹 API
REST API(Representational State Transfer API)
주로 HTTP 프로토콜을 통해 데이터를 교환하는 웹 API입니다. RESTful API는 URL을 통해 자원(Resource)을 식별하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 데이터를 처리합니다.
SOAP API(Simple Object Access Protocol)
XML 기반의 프로토콜을 사용하여 데이터 전송을 처리하는 웹 API입니다.
REST API보다 복잡하지만, 더 엄격한 표준을 따릅니다.
2. 라이브러리 API
개발자가 특정 라이브러리를 사용할 때 제공되는 함수와 명령어를 의미합니다.
예를 들어, Python의 math 라이브러리는 수학 관련 기능을 제공하는 API입니다.
3. 운영 체제 API
운영 체제가 제공하는 기능을 애플리케이션이 사용할 수 있도록 인터페이스를 제공합니다.
예를 들어, Windows API는 파일 시스템 접근, 그래픽 처리, 네트워크 기능 등을 제공합니다.
4. 하드웨어 API
하드웨어 장치(예: 프린터, 그래픽 카드)와 소프트웨어 간의 상호작용을 위한 API입니다.
이를 통해 소프트웨어는 하드웨어의 기능을 직접 조작할 수 있습니다.
3. API의 구성 요소.
1. 요청(Request)
클라이언트가 서버에 데이터를 요청하는 과정입니다.
일반적으로 웹 API에서는 HTTP 요청을 사용하며, 요청에는 다음이 포함됩니다.
URL : 자원의 위치를 지정합니다.
HTTP 메서드 : 클라이언트가 서버에 수행하고자 하는 작업을 나타냅니다.
GET : 데이터를 가져옵니다.
POST : 새로운 데이터를 생성합니다.
PUT : 기존 데이터를 수정합니다.
DELETE : 데이터를 삭제합니다.
헤더(Header) : 요청에 대한 추가 정보를 담고 있으며, 인증 정보나 데이터 형식을 정의합니다.
본문(Body) : 주로 POST 나 PUT 요청에서 서버로 전송할 데이터를 포함합니다.
2. 응답(Response)
서버가 클라이언트의 요청에 대해 반환하는 정보입니다.
응답에는 다음이 포함됩니다.
상태 코드 : 요청이 성공했는지, 오류가 발생했는지를 나타내는 코드입니다.
200 OK : 요청 성공.
404 Not Found : 요청한 자원을 찾을 수 없음.
500 Internal Server Error : 서버 내부 오류.
3. 본문(Body)
서버가 반환하는 실제 데이터로, 보통 JSON, XML, HTML 형식으로 제공됩니다.
4. API의 예시.
1. 웹 API 예시.
Google Maps API
개발자는 Google Maps API를 사용하여 웹 사이트나 앱에 지도를 삽입하고, 위치 기반 데이터를 검색할 수 있습니다.
사용자는 지도 데이터를 직접 관리하지 않아도 Google의 서버에서 제공하는 서비스를 활용할 수 있습니다.
Twitter API
Twitter API는 트위터 데이터(트윗, 사용자 정보 등)에 접근할 수 있는 기능을 제공합니다.
이를 통해 개발자는 트위터와 관련된 애플리케이션이나 분석 도구를 만들 수 있습니다.
라이브러리 API 예시
Java Standard Library
Java 프로그래밍 언어에서 기본적으로 제공하는 함수 및 메서드 모음입니다.
예를 들어, java.util.List API를 통해 리스트 관련 기능을 쉽게 사용할 수 있습니다.
5. API의 장점.
1. 재사용성.
API를 사용하면 기존 시스템의 기능을 재사용할 수 있어, 동일한 기능을 처음부터 개발하지 않아도 됩니다.
이를 통해 개발 효율성이 높아집니다.
2. 확장성.
API는 서로 다른 애플리케이션이 상호작용할 수 있도록 하여, 확장성과 유연성을 제공합니다.
예를 들어, 다양한 앱이 Facebook API를 사용해 소셜 기능을 통합할 수 있습니다.
3. 유지보수.
API는 인터페이스를 제공하는 측(서버, 서비스 제공자)과 이를 사용하는 측(클라이언트)이 독립적으로 유지보수할 수 있게 해줍니다.
즉, API의 구현 세부 사항이 변경되더라도 클라이언트는 동일한 방식으로 API를 호출할 수 있습니다.
4. 보안.
API는 데이터나 서비스에 대한 접근을 제한할 수 있는 보안 메커니즘을 제공합니다.
API 키, OAuth 등의 인증 방식이 이를 지원합니다.
6. API의 보안.
API는 보안 문제를 해결하기 위해 인증(Authentication) 및 권한 부여(Authorization)를 필요로 합니다.
주요 보안 방법은 다음과 같습니다.
API 키
클라이언트가 서버에 요청할 때 사용하는 고유 키로, API 요청이 올바른 애플리케이션에서 왔는지 확인합니다.
OAuth
사용자 인증을 위한 프로토콜로, 사용자가 애플리케이션이 자신의 자원에 접근할 수 있도록 승인하는 시스템입니다.
HTTPS
API 통신 시 암호화를 적용하여 데이터가 안전하게 전송되도록 보장합니다.
7. 요약.
API는 소프트웨어 간의 상호작용을 위한 인터페이스로, 애플리케이션이 서로 데이터를 주고받고 기능을 사용할 수 있게 합니다.
웹, 라이브러리, 운영 체제 등 다양한 API가 있으며, 개발자가 복잡한 기능을 단순화하고 시스템 간의 통합을 쉽게 구현할 수 있도록 돕습니다.
API는 재사용성, 확장성, 보안성 측면에서 중요한 역할을 합니다.
3️⃣ URL(Uniform Resource Locator)
URL(Uniform Resource Locator) 은 인터넷 상에서 특정 자원의 위치를 나타내는 주소입니다.
웹 브라우저는 URL을 사용하여 사용자가 요청한 웹 페이지나 리소스에 접근합니다.
쉽게 말해, URL은 인터넷 상에서 자원이 어디에 위치해 있는지를 나타내는 “웹 주소” 입니다.
1. URL의 구성 요소.
URL은 여러 구성 요소로 이루어져 있으며, 각각의 부분은 자원에 접근하기 위한 중요한 정보를 제공합니다.
일반적인 URL 형식은 다음과 같습니다.
프로토콜://사용자 정보@호스트:포트/경로?쿼리#프래그먼트
1. 프로토콜(Scheme)
클라이언트가 자원에 접근할 때 사용할 통신 방식을 나타냅니다.
예: http, https, ftp 등
예: https://www.example.com에서 https는 HTTPS 프로토콜을 사용해 데이터를 주고받는다는 것을 의미합니다.
2. 호스트(Host)
자원이 위치한 서버의 도메인 이름 또는 IP 주소입니다.
예: www.example.com, 198.168.1.1
예: https://www.example.com에서 www.example.com은 서버의 도메인 이름입니다.
3. 포트 번호(Port) (선택 사항)
서버에서 사용할 특정 네트워크 포트를 지정합니다.
기본적으로 HTTP는 포트 80, HTTPS는 포트 443을 사용합니다.
포트를 명시하지 않으면 기본 포트가 사용됩니다.
예: https://www.example.com:8080에서 8080은 포트 번호입니다.
4. 경로(Path)
서버 내에서 특정 자원의 위치를 나타냅니다.
파일 시스템 경로와 유사하게 자원(웹 페이지, 이미지, 파일 등)의 경로를 지정합니다.
예: https://www.example.com/about 에서 /about은 example.com 서버 내의 about 페이지를 나타냅니다.
5. 쿼리 문자열(Query String)(선택 사항)
추가적인 매개변수(parameters)를 서버에 전달하기 위해 사용됩니다.
주로 URL 끝에 ? 뒤에 위치하며, 여러 개의 매개변수는 &로 구분됩니다.
예: https://www.example.com/search?q=example&lang=en에서 q=example과 lang=en은 검색어와 언어를 나타내는 쿼리 매개변수입니다.
6. 프래그먼트(Fragment)(선택 사항)
페이지 내 특정 위치를 나타냅니다.
# 기호 뒤에 위치하며, 브라우저가 해당 위치로 스크롤하도록 지시합니다.
예: https://www.example.com/docs#section2 에서 #section2는 페이지 내 특정 섹션으로 이동하기 위한 프래그먼트입니다.
2. URL 예시.
다음은 URL의 실제 예시입니다.
https://www.example.com:8080/docs/tutorial.html?name=test#section1
이 URL을 분석하면 다음과 같습니다.
프로토콜 : https(보안된 HTTP 연결)
호스트 : www.example.com (서버의 도메인 이름)
포트 : 8080 (서버가 사용하는 포트 번호)
경로 : /docs/tutorial.html (서버 내 자원의 경로)
쿼리 문자열 : ?name=test (추가 매개변수 name이 test라는 값을 가짐)
프래그먼트 : #section1 (문서 내 특정 색션으로 이동)
3. URL의 역할.
1. 웹 자원 식별
URL은 전 세계적으로 고유한 자원을 식별하여, 사용자가 원하는 웹 페이지나 리소스에 접근할 수 있게 해줍니다.
이는 텍스트 문서, 이미지, 동영상, API 엔드포인트 등 다양한 형태의 자원일 수 있습니다.
2. 데이터 전송
URL은 데이터를 서버로 전송하는 데에도 사용됩니다.
특히 쿼리 문자열을 통해 웹 애플리케이션이 사용자 입력 데이터를 서버로 전송할 수 있습니다.
예를 들어, 검색 엔진에서 검색어를 입력하면 URL에 쿼리 문자열로 서버에 전송됩니다.
3. 네트워크 리소스 연결
URL은 단순한 웹 페이지뿐만 아니라, FTP 서버, 메일 서버, 데이터베이스 등의 네트워크 자원과도 연결될 수 있습니다.
4. URL과 URI의 차이.
URI(Uniform Resource Identifier) 는 URL을 포함하는 더 넓은 개념으로, 자원을 식별할 수 있는 문자열을 말합니다.
URI는 두 가지로 구분됩니다.
URL : 자원의 위치를 나타내는 식별자.
URN(Uniform Resource Name) : 자원의 이름만을 나타내는 식별자.
예: ISBN 번호(urn:isbn:041450523)
URL은 자원의 위치(주소)를 나타내는 URI의 한 형태입니다.
5. URL의 중요성.
1. 웹 탐색.
웹 브라우저에서 URL을 입력함으로써 원하는 웹 페이지에 직접 접근할 수 있습니다.
또한 하이퍼링크로 URL을 연결해 사용자는 쉽게 웹 페이지 간을 이동할 수 있습니다.
2. API 호출.
URL은 웹 API 호출 시 중요한 역할을 합니다.
API는 URL을 통해 특정 자원에 접근하고, 데이터를 주고받는 방식으로 동작합니다.
RESTful API에서는 URL이 자원을 식별하고 작업을 수행하는 중요한 요소입니다.
3. 검색 엔진 최적화(SEO).
URL 구조는 검색 엔진에서 웹 페이지를 크롤링하고 인덱싱하는 데 중요한 요소입니다.
잘 구성된 URL은 검색 엔진이 페이지 내용을 이해하는 데 도움을 줍니다.
6. 요약.
URL은 인터넷에서 자원의 위치를 나타내는 주소로, 프로토콜, 도메인 이름, 경로, 쿼리 문자열, 프래그먼트 등 여러 요소로 구성됩니다.
URL을 통해 사용자는 웹 자원에 접근하고 데이터를 주고받을 수 있으며, 이는 웹 브라우징, API 호출, 데이터 전송 등에 핵심적인 역할을 합니다.
4️⃣ 쿼리(Query)
쿼리(Query) 는 클라이언트가 서버에 데이터를 전달할 떄 URL의 일부 로 포함되어 서버로 전송되는 데이터를 의미합니다.
주로 GET 요청에 사용되며, URL 끝에 ? 기호를 사용하여 쿼리 문자열을 시작하고, 하나 이상의 매개변수(파라미터)를 & 기호로 구분하여 서버에 전달합니다.
1. 쿼리의 주요 개념.
1. GET 요청에서의 쿼리 사용.
GET 요청은 서버에 데이터를 전달할 때 URL에 쿼리 문자열을 포함하여 전송합니다.
쿼리는 URL의 경로 뒤에 붙으며, ? 로 시작한 후 매개변수(key-value)를 =로 연결합니다.
여러 매개변수가 있을 경우 &로 구분합니다.
쿼리는 주로 검색 조건, 필터링 정보, 페이지 번호, 정렬 순서 등 을 서버에 전달할 때 사용됩니다.
https://www.example.com/search?q=shoes&color=red&size=10
예를 들어, 위와 같은 URL을 살펴봅시다.
q=shoes : q라는 매개변수에 shoes라는 검색어를 전달합니다.
color=red : color 라는 매개변수에 red 라는 값을 전달합니다.
size=10 : size 라는 매개변수에 10 이라는 값을 전달합니다.
2. 쿼리의 구조.
?로 시작하여 쿼리 문자열이 시작됩니다.
key=value 형태로 매개변수와 그 값을 표현합니다.
여러 개의 매개변수를 사용할 경우 &로 구분합니다.
기본적인 쿼리 형식은 아래와 같습니다.
https://www.example.com/resource?key1=value1&key2=value2&key3=value3
3. 서버에서의 처리.
서버는 클라이언트가 전달한 쿼리 문자열을 파싱하여 각각의 매개변수 값을 추출합니다.
추출된 값들은 서버에서 요청을 처리하는 데 사용되며, 예를 들어 검색어, 필터링 옵션, 페이지 번호 등을 기반으로 클라이언트가 원하는 결과를 반환합니다.
4. 데이터의 제한.
GET 요청은 데이터의 크기 제한이 있을 수 있습니다.
브라우저나 서버는 보통 약 2048자 정도의 URL 길이를 허용하므로, 쿼리로 전송할 수 있는 데이터는 상대적으로 적습니다.
민감한 데이터(비밀번호, 개인정보 등)는 쿼리 문자열로 전송하지 않는 것이 좋습니다.
쿼리는 URL에 노출되며, 브라우저 기록이나 서버 로그에 저장될 수 있기 때문입니다.
이러한 데이터를 전송하려면 POST 요청을 사용하는 것이 일반적입니다.
2. 쿼리의 사용 예시.
예시 1: 검색.
https://www.example.com/search?query=laptop&category=electronics&sort=price
이 쿼리는 “laptop”이라는 검색어로 전자 제품 카테고리 내에서 가격순으로 정렬된 결과를 요청하는 예입니다.
query : laptop(검색 키워드)
category : electronics(카테고리)
sort : price(가격순 정렬)
예시 2: 필터링 및 페이징.
https://www.example.com/products?category=shoes&brand=nike&size=9&page=2
여기서는 신발 카테고리에서 Nike 브랜드의 사이즈 9인 제품을 두 번째 페이지에서 요청하는 상황입니다.
category : shoes(제품 카테고리)
brand : nike (브랜드 필터)
size : 9 (사이즈 필터)
page : 2 (페이지 번호)
3. 쿼리의 장점.
URL을 통해 간편하게 데이터 전송.
쿼리 문자열을 통해 클라이언트는 쉽게 서버에 데이터를 전달할 수 있습니다.
직접 링크로 공유 가능.
쿼리 문자열을 포함한 URL은 바로 공유할 수 있으며, 사용자가 동일한 URL을 통해 동일한 요청을 재현할 수 있습니다.
예를 들어, 사용자가 필터링 옵션이 적용된 검색 결과 페이지의 URL을 다른 사용자와 공유할 수 있습니다.
검색 엔진 최적화(SEO).
검색 엔진이 쿼리를 통해 특정한 페이지 내용을 이해하고 색인할 수 있습니다.
4. 쿼리와 POST 요청의 차이점.
GET 요청 에서의 쿼리 문자열은 URL에 노출되며, 브라우저 히스토리나 서버 로그에 저장될 수 있습니다.
이 때문에 민감한 정보를 전송할 때는 적합하지 않습니다.
POST 요청 에서는 데이터를 본문(body)에 담아 전송하므로, URL에 노출되지 않습니다.
큰 양의 데이터를 전송하거나 보안이 중요한 정보를 전송할 때는 POST 요청을 사용하는 것이 더 안전합니다.
5. 요약.
쿼리는 클라이언트가 서버에 데이터를 전달할 때 GET 요청의 URL에 매개변수로 포함되어 전송되는 데이터를 의미합니다.
쿼리 문자열은 key=value 형태로 전달되며, 여러 매개변수는 &로 구분됩니다.
검색어, 필터 조건, 페이지 번호 등을 서버로 전송할 때 주로 사용되며 URL에 노출되는 만큼 민감한 정보 전송에는 적합하지 않습니다.
5️⃣ 바디(Body)
바디(Body) 는 클라이언트가 서버로 POST, PUT, PATCH 와 같은 HTTP 요청을 보낼 때, 데이터를 담아 서버로 전송하는 부분 입니다.
특히, 큰 양의 데이터나 보안이 중요한 데이터를 전송할 때 바디를 사용합니다.
바디는 URL에 노출되지 않고 요청 메시지의 본문에 포함되므로, 데이터 전송에 있어 중요한 역할을 합니다.
1. 바디를 사용하는 주요 HTTP 메서드.
1. POST
서버에 새로운 데이터를 생성하기 위한 요청을 보낼 때 사용합니다.
요청 바디에 전송할 데이터를 포함하여 서버로 보냅니다.
예: 사용자 등록, 파일 업로드 등.
2. PUT
서버에 기존 데이터를 수정할 때 사용합니다.
요청 바디에 수정할 데이터를 포함하여 서버로 보냅니다.
예: 기존 사용자의 정보 업데이트.
3. PATCH
서버의 일부 데이터를 수정할 때 사용됩니다.
전체 데이터가 아닌, 수정할 부분만을 바디에 포함하여 보냅니다.
4. DELETE
일부 경우에는 서버에 삭제할 자원에 대한 추가 정보를 바디에 담아 보낼 수 있습니다.
하지만 대부분의 DELETE 요청은 바디 없이 실행되기도 합니다.
2. 바디를 사용하는 이유.
1. 대용량 데이터 전송.
GET 요청에서 사용되는 쿼리 문자열은 데이터 크기에 제한이 있습니다.
반면, 바디를 사용하는 POST나 PUT 요청은 큰 데이터를 안전하게 전송할 수 있습니다.
예를 들어, 대용량 파일이나 이미지 등을 전송할 때 바디를 사용합니다.
2. 보안.
바디에 포함된 데이터는 URL에 노출되지 않으며, 브라우저의 주소 창이나 서버 로그에 기록되지 않습니다.
따라서, 비밀 번호, 개인정보, 결제 정보 등 민감한 데이터를 전송할 때 바디를 사용하는 것이 적절합니다.
3. 구조화된 데이터 전송.
JSON, XML, HTML과 같은 구조화된 데이터를 쉽게 전송할 수 있습니다.
바디는 복잡한 데이터 구조를 담아서 서버로 전달할 수 있는 유연한 방법을 제공합니다.
3. 요청 바디의 형식(Content-Type)
클라이언트가 서버로 데이터를 전송할 때, 요청 바디의 데이터 형식은 Content-Type 헤더로 지정됩니다.
일반적으로 사용되는 데이터 형식은 다음과 같습니다.
1. application/json
JSON(JavaScript Object Notation) 형식으로 데이터를 전송합니다.
RESTful API에서 가장 많이 사용되는 형식입니다.
{
"username": "example_user",
"password": "example_password"
}
2. application/x-www-form-urlencoded
HTML 폼에서 전송되는 기본 형식으로, 키-값 쌍으로 URL 인코딩 방식으로 전송합니다.
데이터는 URL 쿼리 문자열과 유사한 형식으로 바디에 전송됩니다.
username=example_user&password=example_password
3. multipart/form-data
파일 업로드와 같이 다양한 형식의 데이터를 함께 전송할 때 사용됩니다.
각 파일이나 데이터 항목을 별도의 파트로 구분하여 전송할 수 있습니다.
특히 이미지, 문서와 같은 파일을 전송할 때 많이 사용됩니다.
Content-Type: multipart/form-data; boundary=------------------------boundary
--------------------------boundary
Content-Disposition: form-data; name="username"
example_user
--------------------------boundary
Content-Disposition: form-data; name="file"; filename="example.png"
Content-Type: image/png
(파일 데이터)
--------------------------boundary--
4. text/plain
단순 텍스트 데이터를 전송할 때 사용됩니다.
예를 들어, 간단한 메시지를 전송하거나 테스트용으로 사용할 수 있습니다.
4. 요청 바디 사용 예시.
1. POST 요청 예시 (JSON 데이터 전송)
클라이언트가 서버로 사용자 등록 요청을 보낼 때, JSON 형식으로 데이터를 전송할 수 있습니다.
POST /register HTTP/1.1
Host: www.example.com
Content-Type: application/json
{
"username": "new_user",
"email": "user@example.com",
"password": "user_password"
}
Content-Type
요청 바디가 JSON 형식임을 서버에 알립니다.
바디
클라이언트가 전송하려는 데이터가 JSON 형식으로 포함됩니다.
2. PUT 요청 예시 (폼 데이터 전송)
사용자의 정보를 수정할 때, 폼 데이터 형식으로 데이터를 전송할 수 있습니다.
PUT /update-profile HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=updated_user&email=updated@example.com
Content-Type
application/x-www-form-urlencoded 는 키-값 쌍의 폼 데이터를 전송하는 형식입니다.
바디
수정된 사용자 정보를 키-값 쌍으로 포함하고 있습니다.
5. 바디의 보안.
바디를 사용하여 전송되는 데이터는 URL에 노출되지 않지만, 여전히 네트워크를 통해 전송되는 동안 보안이 필요합니다.
이를 위해 HTTPS를 사용하여 통신을 암호화하는 것이 일반적입니다.
HTTPS를 사용하면 바디에 포함된 데이터가 중간에서 가로채이거나 변조되지 않도록 보호할 수 있습니다.
6. 요약.
바디는 클라이언트가 서버로 데이터를 전송할 때 요청 메시지의 본문에 포함되는 부분입니다.
주로 POST, PUT, PATCH와 같은 요청에서 사용되며, 대용량 데이터나 민감한 정보를 안전하게 전송하는 데 적합합니다.
바디에는 JSON, 폼 데이터, 파일 등 다양한 형식의 데이터를 포함할 수 있으며, URL에 노출되지 않아 보안 측면에서 중요한 역할을 합니다.
-
🍃[Spring] `@SpringBootApplication` 애너테이션과 서버(Server)
🍃[Spring] @SpringBootApplication 애너테이션과 서버(Server).
1️⃣ @SpringBootApplication 애너테이션.
@SpringBootApplication 애너테이션은 Spring Boot 애플리케이션에서 자주 사용되는 핵심 애너테이션으로, 여러 가지 기능을 결합한 복합 애너테이션입니다.
이를 통해 Spring Boot 애플리케이션이 자동으로 설정되고 실행됩니다.
1. @SpringBootApplication 세 가지 중요한 애너테이션 결합.
1. @SpringBootConfiguration
Spring의 @Configuration 과 동일한 기능을 제공하며, 이를 통해 Spring 컨텍스트에서 설정 클래스로 인식됩니다.
2. @EnableAutoConfiguration
Spring Boot의 자동 설정 기능을 활성화합니다.
이 기능은 Spring Boot가 클래스패스에 있는 라이브러리들을 바탕으로 자동으로 설정을 적용하여 개발자가 일일이 설정하지 않아도 되도록 도와줍니다.
3. @ComponentScan
현재 패키지와 그 하위 패키지에서 Spring의 컴포넌트들을 자동으로 스캔하고 등록합니다.
즉, @Controller, @Service, @Repository 등의 빈들이 자동으로 Spring 컨텍스트에 등록됩니다.
이 애너테이션은 Spring Boot 애플리케이션의 진입점을 설정하는 메인 클래스에 주로 붙습니다.
이로 인해 애플리케이션이 자동으로 설정되고, 실행될 수 있는 환경이 갖춰집니다.
간단히 말해, @SpringBootApplication 을 통해 개발자는 최소한의 설정으로도 Spring Boot 애플리케이션을 시작하고 구동할 수 있습니다.
2️⃣ 서버(Server)란 무엇인가요?
서버(Server)는 네트워크 상에서 다른 컴퓨터(클라이언트)에게 데이터를 제공하거나, 요청된 서비스를 처리하는 컴퓨터 시스템 또는 소프트웨어를 말합니다.
서버는 클라이언트의 요청을 수신하고, 그 요청에 맞는 데이터를 처리하거나 반환하는 역할을 합니다.
서버는 여러 가지 유형이 있으며, 그 역할에 따라 다양한 기능을 수행합니다.
1. 서버의 기본 기능.
요청 수신.
서버는 클라이언트(웹 브라우저, 모바일 앱 등)로부터 요청을 받습니다.
이 요청은 HTTP, FTP, 또는 다른 프로토콜을 통해 이루어질 수 있습니다.
데이터 처리.
서버는 클라이언트의 요청에 따라 데이터를 검색, 처리 또는 계산합니다.
예를 들어, 데이터베이스에 저장된 정보를 조회하거나, 파일을 업로드하거나 다운로드하는 기능을 수행합니다.
응답 전송.
요청 처리 후, 서버는 클라이언트에게 그 결과를 응답으로 전송합니다.
예를 들어 웹 서버는 HTML 페이지를 반환하거나, API 서버는 JSON 형식의 데이터를 반환할 수 있습니다.
2. 서버의 유형.
웹 서버.
웹 페이지(HTML, CSS, JavaScript)를 클라이언트에 제공하는 서버입니다.
대표적인 예로 Apache HTTP Server, Nginx 등이 있습니다.
데이터베이스 서버.
데이터를 저장하고 관리하며, 클라이언트의 데이터 요청을 처리하는 서버입니다.
MySQL, PostgreSQL, Oracle 등이 이에 해당합니다.
파일 서버.
파일을 저장하고 이를 클라이언트에 제공하는 서버입니다.
주로 FTP(File Transfer Protocol) 서버가 이 역할을 합니다.
애플리케이션 서버.
클라이언트가 요청한 비즈니스 로직을 처리하는 서버입니다.
Spring Boot나 Django 같은 프레임워크를 사용하여 웹 애플리케이션을 구동하는 서버가 이에 해당합니다.
메일 서버.
이메일을 송수신하는 서버로, SMTP, POP3, IMAP 등의 프로토콜을 사용합니다.
3. 서버의 특징.
항상 켜져 있음.
서버는 보통 24시간 내내 작동하여 클라이언트 요청에 신속하게 응답할 수 있어야 합니다.
멀티태스킹.
서버는 동시에 여러 클라이언트의 요청을 처리할 수 있어야 하며, 이를 위해 멀티스레드나 비동기 처리 기술을 사용합니다.
보안.
서버는 민감한 데이터를 처리할 수 있기 때문에 보안이 중요합니다.
SSL/TLS를 통한 암호화, 인증 및 권한 관리 등이 필요합니다.
서버는 인터넷 서비스(웹 사이트, 클라우드 저장소, 이메일 등)를 제공하는 핵심적인 시스템이며, 클라이언트-서버 구조는 오늘날 대부분의 네트워크 기반 애플리케이션에서 사용되는 기본적인 구조입니다.
-
🌐[Network] 라우팅 기능 - 라우팅 시스템
🌐[Network] 라우팅 기능 - 라우팅 시스템.
네트워크 양단에 연결된 호스트들이 전송하는 데이터는 전송 경로 중간에 위치한 라우팅 시스템을 거칩니다.
라우팅 시스템은 데이터를 최종 목적지까지 올바른 경로로 중개하는 교환(Switching) 기능을 제공합니다.
네트워크의 발전이 전화망에서 시작된 까닭에 데이터의 중개 기능을 의미하는 일부 용어에 교환이라는 표현이 굳어지게 되었습니다.
위 그림은 데이터 통신망에서 제공하는 다양한 라우팅 시스템의 종류를 설명합니다.
연결형 서비스를 제공하는 회선 교환 시스템은 아날로그 환경의 음성 전화 서비스를 통해 발전했으며, 고정 대역폭의 전송률을 지원하므로 네트워크의 구조가 상대적으로 단순합니다.
반면에 비연결형 서비스를 제공하는 패킷 교환 시스템은 디지털 환경의 컴퓨터 네트워크를 통해 발전했으며, 가변 대역의 전송률을 지원해 네트워크 구조가 복잡합니다.
이외에도 프레임 릴레이 방식이 있는데, 이는 전송 속도를 향상시키는 기술입니다.
회선 교환(Circuit Switching) 시스템은 고정 대역으로 할당된 연결을 설정하여 데이터 전송을 시작합니다.
따라서 회선에 할당된 고정 크기의 안정적인 전송률로 데이터를 전송할 수 있으며, 연결이 유지되는 동안에는 다른 연결에서 이 대역을 사용할 수 없습니다.
데이터의 전송 경로가 연결 설정 과정에서 확정되므로 라우팅 등의 작업이 상대적으로 쉽습니다.
대표적인 회선 교환 방식인 전화망에서 음성 전송은 안정적인 속도로 일정하게 상대방에세 전달될 수 있지만, 전송 대역이 낭비된다는 단점이 있습니다.
회선 교환 시스템에서는 하나의 연결에 대하여 전송되는 모든 데이터가 동일한 경로로 라우팅됩니다.
패킷 교환(Packet Switching) 시스템은 컴퓨터 네트워크 환경에서 주로 이용합니다.
데이터를 미리 패킷 단위로 나누어 전송하므로 패킷을 기준으로 라우팅이 이루어집니다.
패킷 교환 시스템은 데이터 전송을 위한 전용 대역을 따로 할당하지 않기 때문에 가변 크기의 전송률을 지원합니다.
패킷 교환에는 모든 패킷의 경로를 일정하게 유지시키는 가상 회선(Virtual Circuit) 방식과 패킷들이 각각의 경로로 전송되는 데이터그램(Datagram) 방식이 있습니다.
인터넷을 사용할 때 네트워크의 부하 정도에 따라 전송 속도가 일정하지 않은 이유는 고정적인 전송 대역이 확보되지 않기 때문입니다.
따라서 네트워크의 사용량이 많아지면 전송 속도가 느려집니다.
1️⃣ 라우팅 시스템.
전송 선로를 이용해 데이터를 전송할 때는 전용 회선을 이용하거나 교환 회선을 이용할 수 있습니다.
전용 회선 방식에서는 송신 호스트와 수신 호스트가 전용으로 할당된 통신 선로로 데이터를 전송하지만, 교환 회선 방식에서는 전송 선로 하나를 다수의 호스트가 공유합니다.
일반적으로 전화 서비스와 같은 아날로그 환경의 공중 전화망은 교환 회선 방식을 사용합니다.
위 그림은 교환 회선 방식을 이용하여 네트워크를 구성한 예입니다.
교환 회선 방식을 이용해 데이터를 주고받으려면 중간에 위치한 교환 시스템의 중개가 필요합니다.
그림에서 뭉게구름으로 표현된 네트워크 안에 있는 교환기와 전송 선로들은 바깥에 있는 호스트들이 데이터를 송수신하기 위해 공유하는 자원입니다.
전송 선로는 광케이블 기술 등의 발전으로 물리적인 전송 대역 용량에 비례하여 논리적인 다수의 연결 회선을 지원합니다.
호스트 a에서 호스트 f로 데이터를 보내려면 먼저 연결 설정을 통해 전송 경로를 결정해야 합니다.
외형상 가장 합리적인 경로는 교환기 1 -> 교환기 3 -> 교환기 5 입니다.
만약 교환기 1과 3 사이에 트래픽이 많은 경우라면 교환기 1 -> 교환기 2 -> 교환기 3 -> 교환기 5가 대안이 될 수도 있습니다.
어떤 경로가 더 나을지는 전송 시점의 네트워크 혼잡도 등 여러 요인에 따라 달라집니다.
특정 전송 선로에 데이터가 집중되지 않으면서 효율적인 경로를 선택할 수 있도록 하는 것이 교환기의 중요한 역할입니다.
교환 회선을 이용하는 방식은 데이터의 전송 경로와 관련하여 크게 두 가지로 구분됩니다.
하나는 위 그림의 (a)처럼 데이터를 전송하기 전에 통신 양단 사이에 고정된 연결 경로를 설정하는 회선 교환 방식아고, 다른 하나는 (b)처럼 미리 연결을 설정하지 않고, 데이터를 패킷 단위로 나누어 전송하는 패킷 교환 방식입니다.
이외에 메시지 교환이라는 중간 형태도 있습니다.
(b)의 큰 사각형은 뭉개구름 그림의 교환기에 해당하며, 디지털 통신 환경을 지원하는 인터넷은 패킷 교환 방식의 네트워크에 해당합니다.
1. 회선 교환.
회선 교환(Circuit Switching) 방식은 위 그림의 (a)처럼 통신하고자 하는 호스트가 데이터를 전송하기 전에 연결 경로를 미리 설정하는 방식입니다.
연결 설정 과정에서 송수신 호스트 간의 경로가 결정되기 때문에 모든 데이터가 같은 경로로 전달됩니다.
회선 교환 방식에서는 고정 대역의 논리적인 전송 선로를 전용으로 할당받으므로, 안정적인 데이터 전송률을 지원합니다.
회선 교환 방식에서는 데이터를 패킷 단위로 나누어 전송하지 않기 때문에 패킷이라는 용어를 사용하지 않습니다.
2. 메시지 교환.
메시지 교환(Message Switching)방식은 데이터를 전송하기 전에 경로를 설정하지 않고, 대신 전송하는 메시지의 헤더마다 목적지 주소를 표시하는 방식입니다.
중간의 교환 시스템은 이전 교환 시스템에서 보낸 전체 메시지가 도착할 때까지 받은 메시지를 일시적으로 버퍼에 저장합니다.
이후 모든 메시지가 도착하면 다음 교환 시스템으로 전달하는 방식을 사용하므로 데이터 전송이 교환기 단위로 이어집니다.
따라서 메시지 교환 방식은 송신 호스트가 전송하는 전체 데이터가 하나의 단위로 교환 처리된다고 볼 수 있습니다.
3. 패킷 교환.
위 그림의 (b)와 같은 패킷 교환(Packet Switching) 방식은 회선 교환과 메시지 교환의 장점을 모두 이용합니다.
송신 호스트는 전송할 데이터를 패킷(Packet)이라는 일정 크기로 나누어 전송하며, 원칙적으로 송수신 호스트 사이의 연결이 존재하지 않으므로 각 패킷은 독립적인 라우팅 과정을 거쳐 수신 호스트에 도착합니다.
다만, 패킷 교환 방식에서도 회선 교환 방식과 같은 논리적인 연결 설정 개념을 도입한 가상 회선 방식이 존재합니다.
인터넷에서 사용하는 패킷 교환 시스템의 장점은 전송 대역의 효율적 이용, 호스트의 무제한 수용, 패킷에 우선순위 부여라는 세 가지로 요약할 수 있습니다.
1. 전손 대역의 효율적 이용
여러 호스트에서 전송한 패킷들이 동적인 방식으로 전송 대역을 공유하기 때문에 전송 선로의 이용 효율을 극대화할 수 있습니다.
이를 반대의 관점으로도 설명할 수 있습니다.
즉, 회선 교환 시스템에서는 호스트 간 연결 시 전송 대역을 전용으로 할당하기 때문에 해당 호스트들이 데이터를 전송하지 않더라도 다른 호스트는 이 전송 대역을 이용할 수 없습니다.
결과적으로 회선 교환 시스템은 전송 선로 이용 면에서 비효율적입니다.
2. 호스트의 무제한 수용
회선 교환 방식에서는 개별 연결 요청에 대해 고정 대역을 할당하므로 전송 대역이 부족하면 새로운 연결 설정 요청을 수용할 수 없습니다.
즉, 모든 회선 연결에 할당된 대역의 합이 전체 네트워크의 전송 용량을 초과할 수 없습니다.
그러나 패킷 교환 방식에서는 전송 대역이 부족해 연결 설정 요청을 수용하지 못하는 경우란 없습니다.
임의의 연결 요청에 고정 대역을 할당하지 않으므로 이론상 호스트를 무한히 수용할 수 있습니다.
전송 대역을 사용하는 호스트의 수가 늘면 네트워크 혼잡도가 높아져 패킷의 전송 지연이 심화될 뿐입니다.
3. 패킷에 우선순위 부여
패킷 교환 방식을 이용하면 데이터의 전송 작업이 패킷 단위로 이루어져 패킷에 우선순위를 부여하기 편리합니다.
즉, 특정 호스트가 전송하는 패킷들을 먼저 전송할 패킷과 나중에 전송해도 되는 패킷으로 구분하여 선택적으로 우선순위를 부여할 수 있습니다.
패킷 교환 방식에도 단점은 있습니다.
패킷을 전송하는 과정에서 회선 교환 방식에 비해 더 많은 지연이 발생합니다.
예를 들어, 전송 패킷을 라우터 내부 버퍼에 보관하는 과정에서 지연이 생기고, 기타 여러 종류의 대기 큐를 거치는 과정에서 가변 지연이 생길 수 있습니다.
또한 각각의 전송 패킷이 독립적인 경로로 전달되므로, 패킷마다 전송에 걸리는 시간이 일정하지 않을 수 있습니다.
패킷별로 지연되는 정도를 나타내는 지연 분포의 형태도 가변적일 수밖에 없는데, 이런 가변적인 전송 지연의 분포를 지터(Jiter)라 합니다.
지터 분포는 멀티미티어 데이터와 같이 실시간 처리되는 응용 환경에서 중요합니다.
교환기에서 패킷 경로를 선택하는 방식은 두 가지입니다.
호스트 사이의 전송 경로를 미리 고정하는 정적 경로(Static Routing)와 네트워크 혼잡도를 비롯한 주변 상황에 따라 전송 경로를 지속적으로 조정하는 동적 경로(Dynamic Routing)가 있습니다.
-
🌐[Network] 인터넷 모델과 요약.
🌐[Network] 인터넷 모델과 요약.
인터넷은 데이터의 중개 기능을 담당하는 네트워크 계층으로 IP(Internet Protocol) 프로토콜을 사용하는 네트워크입니다.
따라서 인터넷에 연결하고자 하는 호스트는 반드시 IP 프로토콜을 지원해야 하며, 전송 계층은 TCP(Transmission Control Protocol)나 UDP(User Datagram Protocol)를 사용합니다.
현재 인터넷에서 주로 사용하는 IP 프로토콜은 버전 4(IPv4)입니다.
1️⃣ 구현 환경.
인터넷에 연결된 호스트들의 네트워크 구현 모델에서는 아래 그림과 같이 전송 계층까지의 기능을 시스템 공간인 운영체제 내부에 구현합니다.
즉, 인터넷 환경에서 사용하는 TCP/IP와 하위 계층의 기능을 담당하는 LAN 카드 드라이버 루틴(Driver Routine)은 운영체제 영역에 속합니다.
TCP/IP를 이용하려면 소켓 기능이 필요하며, 사용자 공간에서 네트워크 응용 기능을 지원하는 프로그램을 작성해야 합니다.
예를 들어, 스마트폰에서 실행되는 네트워크 관련 앱들이 네트워크 프로세스에 해당됩니다.
1. 시스템 공간.
TCP와 UDP 프로토콜은 시스템 운영체제인 커널(Kernel) 내부에 구현되므로 일반 사용자가 이 기능을 직접 이용할 수는 없습니다.
대신 소켓(Socket) 인터페이스라는 전송 계층의 프리미티브를 이용해야 하는데, 소켓은 운영체제에서 시스템 콜 기능으로 구현되므로 사용자 프로그램에서 이를 함수 호출 방식으로 사용합니다.
TCP는 연결형 서비스를 제공하고, UDP는 비연결형 서비스를 제공합니다.
인터넷에서 네트워크 계층은 IP(Internet Protocol)로 구현되며 전송 패킷의 올바른 경로 선택 기능을 제공합니다.
네트워크 프로세스를 실행하는 호스트 사이에는 다수의 라우터가 존재하며, 이 라우터들은 IP 프로토콜을 이용하여 패킷 중개 기능을 수행합니다.
인터넷은 라우터들의 집합이며, 스마트폰을 포함한 일반 호스트들은 인터넷의 끝단에 연결되는 종단 장치에 해당합니다.
네트워크 계층 아래의 계층들은 LAN 카드와 LAN 카드를 구동하는 드라이버 루틴에 의해 구현됩니다.
2. 사용자 공간.
세션 계층부터 응용 계층까지의 기능은 사용자 응용 프로그램으로 구현됩니다.
프로그래밍 환경에서 전송 계층의 기능을 제공하는 소켓 시스템 콜을 호출해 TCP와 UDP 기능을 사용할 수 있습니다.
소켓 시스템은 유닉스(Unix), 리눅스(Linux), 윈도우즈(Windows) 운영체제 등 인터넷에 접곳 가능한 모든 호스트에서 제공합니다.
프로그램에서 소켓을 사용할 때는 소켓마다 부여되는 고유 주소인 포트 번호를 관리해야 합니다.
IP 주소는 호스트를 구분하는 주소 역할을 하지만, 포트 번호는 특정 호스트에서 실행되는 네트워크 프로세스를 구분하는 주소입니다.
일반 네트워크 프로세스는 포트 하나를 할당해 사용하므로 포트 번호와 네트워크 프로세스가 일대일로 대응됩니다.
응용 환경에 따라서는 포트 번호를 여러 개 할당할 수도 있습니다.
응용 네트워크 프로그램을 설계할 때는 포트 할당에 주의해야 하지만, 일반 사용자는 프로그램 하나에 포트 하나를 사용한다고 가정해도 큰 문제가 없습니다.
인터넷 응용 프로세스의 고유 주소는 IP 주소와 포트 번호의 조합으로 완성됩니다.
상위 계층은 위 그림처럼 사용자 프로그램 공간에서 구현됩니다.
네트워크 응용 프로그램으로는 전통적인 텔넷(Telnet), FTP, 웹 브라우저 등을 포함하여 스마트폰과 일반 PC에서 실행되는 수많은 앱이 있는데, 모두 TCP와 UDP를 사용해 네트워크에 연결합니다.
2️⃣ 프로토콜.
인터넷에서 데이터 전송은 계층 4의 TCP와 UDP 프로토콜, 계층 3의 IP 프로토콜에 의해 이루어집니다.
인터넷 모델에서는 사용자 데이터의 전송이 TCP, UDP, IP 프로토콜에 의해 이루어지지만, 이들이 올바르게 동작하려면 더 많은 제어용 프로토콜이 필요합니다.
특히, 주소 문제를 해결하기 위한 ARP/RARP 프로토콜과 오류 문제를 해결하기 위한 ICMP 프로토콜은 인터넷 모델의 동작에서 매우 중요한 역할을 합니다.
1. 프로토콜 계층 구조.
아래의 그림은 TCP/IP를 사용하는 인터넷 환경에서 관련 프로토콜들이 계층 구조를 설명합니다.
맨 위의 응용 프로세스는 TCP와 UDP를 사용해 데이터 송수신 기능을 수행하지만, 특별한 환경에서 네트워크 계층의 IP 프로토콜을 직접 사용하기도 합니다.
ICMP와 ARP/RARP는 사용자 데이터를 전송하지 않기 때문에 특정 계층으로 설명하기 애매하지만, 일반적으로 네트워크 계층으로 분류합니다.
ICMP와 ARP/RARP가 전송하는 제어용 데이터들은 같은 계층에 속하는 IP 프로토콜의 도움을 받는다는 점이 특별합니다.
그림에 표시하지 않았지만 인터넷의 제어용 프로토콜은 그 종류가 매우 다양합니다.
네트워크 계층의 IP는 사용자 데이터를 전송하는 프로토콜입니다.
IP의 동작 과정에서 전송 오류가 발생하는 경우에 대비해 오류 정보를 전송하는 목적으로 ICMP(Internet Control Message Protocol)를 사용합니다.
ICMP는 IP 프로토콜과 같은 계층으로 간주할 수 있지만, ICMP에서 발생하는 ICMP 메시지는 IP 패킷에 캡슐화되어 전송됩니다.
2. ARP와 RARP
인터넷 모델에서 사용하는 주소는 데이터 링크 계층의 MAC 주소, 네트워크 계층의 IP 주소, 전송 계층의 포트 번호입니다.
인터넷에서 통신하기 위해서는 송신 호스트가 자신의 세 가지 주소뿐만 아니라, 수신 호스트의 세 가지 주소도 모두 알아야 합니다.
편지를 보내기 위해서는 자신의 집 주소와 수신자의 집 주소를 모두 알아야 하는 것과 같습니다.
먼저, 포트 번호는 사용자 프로그램 환경에서 사용되므로, 번호 할당과 관리가 다른 계층 프로토콜의 동작에 크게 영향을 미치지 않습니다.
IP 주소와 MAC 주소는 프로토콜의 동작 특성상 몇 가지 고려할 사항이 있습니다.
예를 들어, 계층 2 프로토콜을 이용해 데이터를 전송하려면 수신 호스트의 MAC 주소가 필요합니다.
일반적으로 송신 호스트는 자신의 IP 주소와 MAC 주소는 쉽게 얻을 수 있습니다.
즉, 자신의 IP 주소는 호스트의 네트워크 설정 과정에서 하드디스크에 저장되며, MAC 주소는 LAN 카드 내에 기록되어 있습니다.
그렇지만 수신 호스트의 주소를 얻으려면 몇 단계의 처리 과정이 필요합니다.
먼저, 수신 호스트의 IP 주소는 응용 프로그램의 사용자로부터 입력된 호스트 이름을 IP 주소로 변환하여 얻을 수 있습니다.
하지만 수신 호스트의 MAC 주소 정보는 어디에서도 얻을 수 없습니다.
따라서 사용자로부터 입력된 수신 호스트의 IP 주소를 이용해 MAC 주소를 구하는 기능이 필요한데 ARP(Address Resolution Protocol)가 이 기능을 담당합니다.
호스트의 IP 주소는 컴퓨터 설정 작업의 초기화 과정에서 특정 파일에 보관됩니다.
그러나 파일 시스템이 존재하지 않는 특수 목적의 시스템은 LAN 카드에 내장된 자신의 MAC 주소는 알지만, 자신의 IP 주소는 모르는 경우가 있습니다.
이 문제를 해결하기 위하여 MAC 주소를 IP 주소로 변환하는 RARP(Reverse Address Resolution Protocol)가 필요합니다.
3. ICMP
데이터 전송 프로토콜인 IP가 동작하는 과정에서는 전송 오류가 발생할 수 있습니다.
오류가 발생하면 반드시 송신자에게 회신해 복구 작업을 하게 해야 하는데, 이 작업은 ICMP(Internet Control Message Protocol)가 담당합니다.
ICMP 프로토콜은 오류 메시지를 전송하기 위한 별도의 헤더 구조를 가지며, IP 패킷에 캡슐화되어 전송되지만 IP와 같은 계층으로 취급됩니다.
ARP, RARP, ICMP의 특징을 요약하여 설명하면 아래 표와 같습니다.
프로토콜
특징
ARP
인터넷에서 통신하려면 자신의 로컬 IP 주소와 MAC 주소, 수신 호스트의 IP 주소와 MAC 주소가 필요합니다. ARP(Address Resolution Protocol)는 수신 호스트의 주소 변환 기능을 제공하는데, 사용자가 입력한 원격 IP 주소를 이용해 원격 MAC 주소를 제공하는 프로토콜입니다.
RARP
RARP(Reverse Address Resolution Protocol)는 로컬 호스트의 주소 변환 기능을 제공하는데, LAN 카드에 보관된 MAC 주소를 이용해 자신의 IP 주소를 얻어내는 프로토콜입니다. 일반 컴퓨터 시스템은 로컬 호스트의 IP 주소가 파일 시스템에 보관되므로 RARP를 사용하지 않지만, 파일 시스템이 존재하는 시스템에서는 RARP를 반드시 사용해야 합니다.
ICMP
사용자 데이터의 전송 과정에서 오류가 발생하면 오류 메세지가 생성됩니다. ICMP(Internet Control Message Protocol)는 이 오류 메시지를 송신 호스트에 전송하는 기능을 담당하는 프로토콜입니다.
3️⃣ 요약.
1. 모듈화
일반적으로 복잡하고 큰 시스템의 기능은 특정 단위의 모듈로 나뉘어 설계합니다.
시스템을 기능별로 모듈화하면 시스템 구조가 단순해져서 전체 시스템을 이해하기 쉽습니다.
또한 각 단위 모듈이 독립적인 기능을 수행하기 때문에 고장이나 업그레이드 등의 상황에 손쉽게 대처할 수 있습니다.
2. 계층 구조.
분할된 모듈들은 협력 관계를 유지하면서 유기적으로 동작합니다.
네트워크에서는 독립적인 고유 기능을 수행하는 모듈들이 상하 계층 구조로 연결되어 동작합니다.
계층 구조에서는 상위 계층이 하위 계층에 특정 서비스를 요청하는 방식으로 동작합니다.
요청을 받은 하위 계층은 해당 서비스를 실행하여 그 결과를 상위 계층에 돌려줍니다.
3. 주소의 표현.
여러 호스트가 연결된 환경에서 특정 호스트끼리 통신하려면 상대방을 구분할 수 있는 방법이 필요합니다.
시스템을 구분하여 지칭하기 위해서 이름을 부여하는 것을 주소 체계라 합니다.
일반적으로 호스트에 주소를 하나씩 부여하지만, 다수의 호스트를 묶어 그룹 주소로 표기하지고 합니다.
일대다 통신의 대표적인 표기 방법에는 네트워크에 연결된 모든 호스트에 데이터를 전송할 수 있는 브로드캐스팅과 특정 사용자를 그룹으로 묶어서 지칭하는 멀티캐스팅이 있습니다.
4. 오류 제어.
네트워크에서는 데이터 송수신 과정에서 오류가 발생할 수 있습니다.
정송 오류에는 데이터가 깨져서 도착하는 데이터 변형 오류와 데이터가 도착하지 못하는 데이터 분식 오류가 있습니다.
오류가 발생하는 1차 원인은 물리 계층의 전송 매체에 의한 물리적인 오류입니다.
데이터가 변형되거나 분실되는 오류를 해결하려면 먼저 오류가 발생한 사실을 인지해야 합니다.
네트워크에서 전송 오류를 해결하는 방법은 송신 호스트가 원래 데이터를 재정손하는 것입니다.
5. 흐름 제어.
전송 매체에서 물리적인 오류가 없었는데도 데이터를 분실하는 경우가 있는데, 이는 송수신 호스트 사이의 데이터 전송/처리 속도 차이 때문에 발생하기도 합니다.
수신 호스트의 버퍼 처리 속도보다 송신 호스트가 데이터를 전송하는 속도가 빠르면 논리적인 데이터 분실 오류가 발생할 수 있습니다.
이 문제를 해결하려면 송신 호스트의 전송 속도를 조절하는 흐름 제어 기능이 필요합니다.
6. 서비스 프리미티브.
프로토콜은 계층 구조로 이루어져 있고, 하위 계층이 상위 계층에 서비스를 제공하는 방식으로 동작합니다.
이러한 서비스는 프리미티브 형태로 구현되며, 연결형 서비스에서 자주 사용하는 프리미티브의 종류에는 CONNECT, DATA, DISCONNECT가 있습니다.
통신 프로토콜에서 서비스 프리미티브를 올바르게 수행하려면 각 프리미티브가 Request, Indication, Response, Confirm이라는 네 가지 기능을 포함하도록 설계해야 합니다.
클라이언트가 서버에 전달하는 요청은 Request와 Indication으로 구현되고 서버의 응답은 Response와 Confirm으로 구현됩니다.
7. OSI 7계층 모델.
네트워크에 연결된 컴퓨터들이 데이터를 주고받으려면 서로 연동할 수 있게 표준화된 인터페이스를 지원해야 합니다.
국제 표준화 기구인 ISO가 확립한 OSI 7계층 모델은 개방화된 데이터 통신 환경에 적합한 계층적 구현 모델의 표준입니다.
연결된 두 호스트가 각각 7개 계층으로 구성된 모듈을 수행함으로써 데이터 송수신이 가능합니다.
전송 데이터는 송신 호스트의 응용 계층에서 시작해 하위 계층으로 순차적으로 전달되어, 최종적으로 물리 계층에서 수신 호스트에 전달됩니다.
수신 호스트에서는 데이터를 상위 계층으로 순차적으로 이동시켜 응용 계층까지 보내줍니다.
8. 인터넷 모델.
인터넷은 데이터의 중개 기능을 담당하는 네트워크 계층으로 IP 프로토콜을 사용하는 네트워크입니다.
인터넷에 연결된 컴퓨터의 네트워크 구현 모델에서는 전송 계층까지의 기능을 시스템 공간인 운영체제 내부에 구현합니다.
TCP와 UDP는 시스템 운영체제인 커널 내부에 구현되므로, 소켓 인터페이스라는 전송 계층의 프리미티브를 사용자 프로그램에서 호출하는 방식으로 사용합니다.
세션 계층부터 응용 계층까지의 기능은 사용자 프로그램으로 구현됩니다.
9. ARP/ RARP
인터넷 모델에서 사용하는 주소는 데이터 링크 계층의 MAC 주소, 네트워크 계층의 IP 주소, 전송 계층의 포트 번호입니다.
계층 2 프로토콜을 이용해 데이터를 전송하려면 수신 호스트의 MAC 주소가 필요합니다.
일반적으로 송신 호스트는 자신의 IP 주소와 MAC 주소는 쉽게 얻을 수 있지만, 수신 호스트의 MAC 주소를 얻으려면 몇 단계의 처리 과정이 필요합니다.
사용자로부터 입력된 수신 호스트의 IP 주소를 이용해 MAC 주소를 구하는 기능이 필요한데, ARP가 이 기능을 담당합니다.
파일 시스템이 없는 호스트는 LAN 카드에 내장된 자신의 MAC 주소는 알지만 파일 시스템이 존재하지 않으므로 자신의 IP 주소를 알 수 없습니다.
이 문제를 해결하기 위하여 MAC 주소를 IP 주소로 변환하는 RARP가 필요합니다.
10. ICMP
데이터 전송 프로토콜인 IP가 동작하는 과정에서는 전송 오류가 발생할 수 있습니다.
오류가 발생하면 반드시 송신 호스트에 회신하여 복구 작업을 할 수 있게 해야 하는데, 이 작업은 ICMP가 담당합니다.
ICMP 프로토콜은 오류 메시지를 전송하기 위한 별도의 헤더 구조를 가지며, IP 패킷에 캡슐화되어 전송되지만 IP 프로토콜과 같은 계층으로 취급됩니다.
-
💾 [CS] MVP 패턴.
💾 [CS] MVP 패턴.
1️⃣ MVP 패턴.
MVP 패턴은 MVC 패턴으로부터 파생되었으며 MVC에서 C에 해당하는 컨트롤러가 프레젠터(Presenter)로 교체된 패턴입니다.
뷰와 프레젠터는 일대일 관계이기 때문에 MVC 패턴보다 더 강한 결합을 지닌 디자인 패턴이라고 볼 수 있습니다.
2️⃣ 자바에서의 MVP 패턴.
자바에서의 MVP 패턴은 주로, 데스크탑 애플리케이션(JavaFX, Swing) 또는 안드로이드 애플리케이션을 개발할 때 많이 사용됩니다.
자바에서는 주로 MVC 패턴이 많이 사용되지만, MVP 패턴은 UI와 비즈니스 로직을 더욱 명확하게 분리할 수 있기 때문에 상황에 따라 더 적합할 수 있습니다.
3️⃣ MVP 패턴의 구조.
MVP는 Model-View-Presenter의 약자로, 아래와 같은 세 가지 주요 구성 요소로 나뉩니다.
1. Model(모델)
애플리케이션의 데이터와 비즈니스 로직을 처리합니다.
데이터베이스와 상호작용하고 데이터를 가공하는 역할을 담당합니다.
예: 데이터베이스 접근, API 호출, 데이터 가공.
2. View(뷰)
사용자 인터페이스(UI)를 담당하며, 사용자가 보는 화면을 표시하고 입력을 받습니다.
View는 Presenter에 의존하여 데이터를 요청하고, 그 데이터를 표시하는 역할을 합니다.
예: 자바의 JPanel, JFrame(Swing) 또는 Activity, Fragment(안드로이드)
3. Presenter(프레젠터)
View와 Model간의 중재자 역할을 하며, 뷰에서 발생한 사용자 상호작용을 처리하고, 필요한 데이터를 모델에서 가져와 뷰에 전달합니다.
비즈니스 로직을 처리하며, View와 Model을 직접 연결하지 않고 독립적으로 관리합니다.
Presenter는 View 인터페이스를 통해 View와 통신하고, 테스트 가능한 구조를 만듭니다.
4️⃣ 백엔드를 Java로 구현시 MVP 패턴이 사용되나요?
일반적으로 MVP패턴(Model-View-Presenter) 은 주로 프론트엔드 또는 UI 중심 애플리케이션에서 사용됩니다.
MVP 패턴은 사용자 인터페이스와 비즈니스 로직을 분리하는 데 중점을 두기 때문에, 데스크탑 애플리케이션(JavaFX, Swing)이나 모바일 애플리케이션(안드로이드)에서 많이 사용됩니다.
따라서 백엔드 애픝리케이션을 Java로 구현할 때는 MVP 패턴이 거의 사용되지 않으며, 그 대신 다른 디자인 패턴이 주로 사용됩니다.
1. MVP 패턴의 목적.
MVP 패턴은 기본적으로 사용자 인터페이스(UI)를 중심으로 View와 비즈니스 로직(Presenter) 을 분리하는 데 목적이 있습니다.
하지만 백엔드 애플리케이션은 사용자 인터페이스가 아닌 서버 측 비즈니스 로직, 데이터 처리, API 제공 등을 다루기 때문에, UI 요소가 존재하지 않습니다.
따라서 View 라는 개념이 백엔드에 적합하지 않습니다.
2. 백엔드에서는 MVC 패턴이 더 적합.
Java 기반 백엔트 개발에서는 MVC(Model-View-Controller) 패턴 이나 서비스 계층 패턴 과 같은 구조가 더 일반적입니다.
특히, Spring Framework 같은 인기 있는 백엔드 프레임워크에서는 MVC 패턴이 기본적으로 사용됩니다.
백엔드에서 컨트롤러(Controller) 가 클라이언트의 요청을 처리하고, 모델(Model) 이 데이터 처리와 비즈니스 로직을 담당하며, 뷰(View) 는 API 응답(JSON, XML 등)을 생성하는 역할을 합니다.
백엔드에서 자주 사용되는 디자인 패턴.
1. MVC 패턴(Model-View-Controller)
서버 요청을 처리하고, 데이터베이스와 상호작용하며, API 응답을 생성하는 데 사용됩니다.
2. 서비스 계층 패턴
비즈니스 로직을 서비스 계층으로 분리하여 재사용성과 유지보수성을 높이는 패턴입니다.
3. Repository 패턴
데이터베이스 액세스 로직을 추상화하여, 비즈니스 로직과 데이터 액세스를 분리합니다.
4. Command 패턴
사용자의 요청이나 명령을 객체로 변환하여 처리하는 방식으로, 여러 요청을 관리하는데 유리합니다.
5. Observer 패턴
상태 변화를 여러 객체가 구독하고 반응하는 패턴으로, 이벤트 기반 시스템에 자주 사용됩니다.
3. 백엔드에서 사용하는 디자인 패턴의 예시.
1. Spring MVC 패턴
Spring에서는 Controller가 HTTP 요청을 받고, Service에서 비즈니스 로직을 처리한 뒤, Model을 사용하여 데이터를 전달하고 View를 반환하는 전형적인 MVC 패턴을 사용합니다.
여기서 View는 HTML 또는 JSON, XML과 같은 응답 포맷을 의미합니다.
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findUserById(id);
return new ResponseEntity<>(user, HttpStatus.OK);
}
}
Controller 는 클라이언트 요청을 처리하고, 데이터를 가공한 후 응답합니다.
Service 는 비즈니스 로직을 처리하는 중간 계층 역할을 합니다.
Repository 는 데이터베이스와 상호작용하는 부분입니다.
2. 서비스 계층 패턴
서비스 계층을 사용하면 컨트롤러가 직접 비즈니스 로직을 다루지 않고, 서비스 클래스가 이를 처리합니다.
이로 인해 코드가 더 구조적으로 관리되고 테스트 가능성이 높아집니다.
```java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
}
}
```
3. Repository 패턴
데이터베이스 관련 로직을 별도의 Repository 인터페이스로 분리하여 데이터 엑세스를 쉽게 관리하고 추상화할 수 있습니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 데이터베이스 접근 로직을 추상화
}
4. MVP 패턴이 백엔드에서 적합하지 않은 이유.
View에 대한 의존성.
MVP 패턴의 핵심 요소는 View 이며, 백엔드에는 UI를 다루지 않기 때문에 View의 역할이 존재하지 않습니다.
백엔드는 사용자 인터페이스를 렌더링하거나 다루지 않고, 데이터를 처리하고 클라이언트는 응답을 반환하는 역할을 합니다.
분리된 로직.
백엔드는 클라이언트와 데이터를 주고받으며, 이 과정에서 비즈니스 로직, 데이터 엑세스, API 응답 생성 등과 같은 복잡한 처리가 이루어 집니다.
이러한 작업을 관리하는 데는 MVC 패턴 이나 레이어드 아키텍처 가 더 적합합니다.
-
💾[Database] SQL 쿼리.
💾[Database] SQL 쿼리 - 스키마 삭제, 테이블 생성.조회.수정
1️⃣ 스키마 삭제.
MySQL에서 스키마(schema) 또는 데이터베이스(database) 를 삭제하는 방법은 매우 간단합니다.
스키마와 데이터베이스는 사실상 같은 개념으로 취급되며, MySQL에서는 주로 “데이터베이스” 라는 용어를 사용합니다.
스키마를 삭제하기 위해서는 DROP DATABASE 명령을 사용합니다.
이 명령어는 지정한 데이터베이스를 완전히 삭제하며, 해당 데이터베이스 안의 모든 테이블과 데이터도 함께 삭제됩니다.
스키마(데이터베이스) 삭제하는 명령어
DROP DATABASE database_name;
여기서 database_name은 삭제하려는 데이터베이스(스키마)의 이름입니다.
예시.
DROP DATABASE mydatabase;
위 명령어를 실행하면 mydatabase 라는 이름의 데이터베이스가 삭제됩니다.
삭제 전에 주의할 점.
1. 백업
데이터베이스가 삭제되면 그 안에 있는 모든 데이터가 영구적으로 사라집니다.
필요한 데이터가 있다면 먼저 백업을 하는 것이 좋습니다.
2. 존재 여부 확인
데이터베이스가 존재하지 않을 경우 에러가 발생할 수 있습니다.
이를 방지하기 위해 데이터베이스가 존재하는 경우에만 삭제하는 명령어를 사용할 수 있습니다.
DROP DATABASE IF EXISTS mydatabase;
이 명령어는 mydatabase가 존재할 경우에만 삭제하고, 존재하지 않을 경우에는 아무 작업도 하지 않으며 에러 메시지를 출력하지 않습니다.
MySQL에서 데이터베이스 확인 및 삭제 과정.
1. 현재 데이터베이스 목록 확인
데이터베이스 목록을 확인하려면 아래 명령어를 사용합니다.
SHOW DATABASES;
2. 데이터베이스 삭제
데이터 베이스 이름을 확인한 후, DROP DATABASE 명령을 사용하여 삭제합니다.
3. 삭제 확인
다시 데이터베이스 목록을 확인하여 삭제된 데이터베이스가 목록에 없는지 확인합니다.
SHOW DATABASES;
위 과정으로 MySQL에서 안전하게 스키마(데이터베이스)를 삭제할 수 있습니다.
2️⃣ 테이블 생성.
MySQL에서 shopdb 라는 데이터베이스 안에 memberTBL이라는 테이블을 생성하는 방법은 다음과 같습니다.
1. MySQL에서 데이터베이스 선택.
먼저 shopdb 데이터베이스를 사용해야 합니다.
MySQL에서 작업할 데이터베이스를 선택하려면 USE 명령을 사용합니다.
USE shopdb;
이 명령을 실행하면 MySQL은 shopdb 데이터베이스를 사용하여 이후의 모든 작업을 수행합니다.
2. memberTBL 테이블 생성.
memberTBL 테이블을 만들기 위해 CREATE TABLE 명령을 사용합니다.
테이블은 다양한 컬럼(필드)로 구성되며, 각 컬럼은 이름과 자료형을 가집니다.
예시로, 회원 정보를 저장하는 테이블을 만들어 보겠습니다.
이 테이블은 회원의 ID, 이름, 이메일, 가입일 등을 저장한다고 가정하겠습니다.
CREATE TABLE memberTBL (
memberID INT AUTO_INCREMENT PRIMARY KEY, -- 회원 ID(자동 증가)
memberName VARCHAR(50) NOT NULL, -- 회원 이름
memberEmail VARCHAR(100), -- 회원 이메일
joinDate DATE -- 가입 날짜
);
테이블 생성 코드 설명
memberID INT AUTO_INCREMENT PRIMARY KEY
회원 ID는 정수형(INT)이고, 자동으로 증가하는 값(AUTO_INCREMENT)입니다.
또한, 기본 키(PRIMARY KEY)로 설정되어 각 회원이 고유한 값을 가집니다.
memberName VARCHAR(50) NOT NULL
회원 이름은 최대 50자 문자열(VARCHAR)로 저장되며, 비어있을 수 없습니다(NOT NULL).
memberEmail VARCHAR(100)
회원 이메일은 최대 100자의 문자열로 저장됩니다.
필수 입력은 아닙니다.
joinDate DATE
가입 날짜는 DATE 형식으로 저장됩니다.
3. 테이블 확인.
테이블이 정상적으로 생성되었는지 확인하려면 SHOW TABLES 명령을 사용합니다.
SHOW TABLES;
memberTBL이 목록에 나타나면 테이블이 성공적으로 생성된 것입니다.
4. 테이블 구조 확인.
테이블의 구조를 확인하려면 DESCRIBE 명령을 사용합니다.
DESCRIBE memberTBL;
이 명령어는 테이블의 각 컬럼 이름, 데이터 타입, 기본값, 인덱스 등을 보여줍니다.
5. 결론.
위 과정을 통해 MySQL에서 shopdb라는 데이터베이스에 memberTBL이라는 테이블을 생성할 수 있습니다.
테이블 구조는 필요에 따라 수정할 수 있으며, 각 필드는 원하는 데이터 타입과 제약 조건을 적용할 수 있습니다.
3️⃣ 테이블 조회.
MySQL에서 테이블 내부의 데이터를 조회하려면 SELECT 문을 사용합니다.
SELECT 문은 테이블에 저장된 데이터를 검색하여 출력하는 역할을 합니다.
1. 테이블의 모든 데이터를 조회하는 방법.
memberTBL 테이블의 모든 데이터를 확인하려면 아래 명령어를 사용합니다.
SELECT * FROM memberTBL;
이 명령어는 memberTBL 테이블에 있는 모든 행(Row)과 열(Column)을 가져옵니다.
2. 특정 컬럼(Column,열)만 조회하는 방법.
특정 컬럼(Column, 열)만 조회하고 싶다면 열 이름을 지정하여 SELECT 문을 작성할 수 있습니다.
SELECT memberID, memberName FROM memberTBL;
위 명령어는 memberID와 memberName 컬럼에 해당하는 값들만 출력합니다.
3. 조건에 맞는 데이터를 조회하는 방법.
특정 조건에 맞는 데이터만 조회하고 싶을 때는 WHERE 절을 사용합니다.
예를 들어, memberID 가 ‘user01’인 데이터를 조회하려면 아래와 같이 할 수 있습니다.
SELECT * FROM memberTBL WHERE memberID = 'user01';
4. 테이블의 구조를 확인하는 방법.
테이블의 구조(열 이름, 데이터 타입 등)를 확인하려면 DESCRIBE 또는 SHOW COLUMNS 명령을 사용할 수 있습니다.
DESCRIBE memberTBL;
또는
SHOW COLUMNS FROM memberTBL;
이 명령어들은 테이블의 각 열에 대한 정보(열 이름, 데이터 타입, NULL 허용 여부 등)를 보여줍니다.
이 명령어들을 통해 memberTBL 테이블에 저장된 데이터를 쉽게 확인할 수 있습니다.
4️⃣ 테이블 수정.
이미 생성된 테이블에서 memberID를 기본 키(Primary Key)로 설정하려면 ALTER TABLE 명령을 사용하여 테이블을 수정할 수 있습니다.
아래의 SQL 명령어를 사용하여 memberID를 기본 키로 설정할 수 있습니다.
1. ALTER TABLE을 사용하여 Primary Key 추가.
ALTER TABLE memberTBL
ADD PRIMARY KEY (memberID);
이 명령어는 기존의 memberTBL 테이블에서 memberID 컬럼을 기본 키로 설정합니다.
2. 테이블 구조 확인
DESCRIBE 명령을 사용하여 테이블 구조를 확인하고 memberID가 기본 키로 설정되었는지 확인할 수 있습니다.
DESCRIBE memberTBL;
3. 주의 사항.
memberID 컬럼에 중복된 값이 없어야 하며, NULL 값도 없어야 합니다.
기본 키는 고유해야 하므로 중복된 값이나 NULL 값이 존재하면 에러가 발생할 수 있습니다.
만약 테이블에 이미 중복된 값이 있거나 NULL이 포함된 경우, 기본 키를 추가하기 전에 데이터를 정리해야 합니다.
4. 추가 예시 : 새로운 테이블에서 Primary Key 지정
만약 테이블을 처음부터 생성하면서 memberID를 기본 키로 설정하려면 다음과 같이 할 수 있습니다.
CREATE TABLE memberTBL (
memberID CHAR(8) NOT NULL PRIMARY KEY, -- 8글자, 기본키
memberName CHAR(5) NOT NULL, -- 5글자, NULL 허용 안 함
memberAddress CHAR(20) NULL -- 20글자, NULL 허용
);
이렇게 하면 테이블을 생성할 때 memberID가 기본 키로 설정됩니다.
-
☕️[Java] Class 클래스
☕️[Java] Class 클래스.
자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다.
Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다.
1️⃣ Class 클래스의 주요 기능
타입 정보 얻기.
클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다.
리플랙션.
클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다.
동적 로딩과 생성.
Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다.
애노테이션 처리.
클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다.
예시.
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다.
package langReview.clazz;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
Class clazz = String.class; // 1. 클래스에서 조회.
// Class clazz = new String().getClass(); // 2. 인스턴스에서 조회.
// Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
// 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field);
System.out.println("field.getType() = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass());
// 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interfaces) {
System.out.println("Interface: " + i.getName());
}
}
}
class vs clazz
class는 자바의 예약어입니다. 따라서 패키지명, 변수명으로 사용할 수 없습니다.
이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용합니다.
clazz는 class와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있습니다.
주의!
main() 옆에 throws Exception이 추가된 부분에 주의합시다.
이 코드가 없으면 컴파일 오류가 발생합니다.
실행 결과
field = private final byte[] java.lang.String.value
field.getType() = class [B value
field = private final byte java.lang.String.coder
field.getType() = byte coder
field = private int java.lang.String.hash
field.getType() = int hash
field = private boolean java.lang.String.hashIsZero
field.getType() = boolean hashIsZero
field = private static final long java.lang.String.serialVersionUID
field.getType() = long serialVersionUID
field = static final boolean java.lang.String.COMPACT_STRINGS
field.getType() = boolean COMPACT_STRINGS
field = private static final java.io.ObjectStreamField[] java.lang.String.serialPersistentFields
field.getType() = class [Ljava.io.ObjectStreamField; serialPersistentFields
field = private static final char java.lang.String.REPL
field.getType() = char REPL
field = public static final java.util.Comparator java.lang.String.CASE_INSENSITIVE_ORDER
field.getType() = interface java.util.Comparator CASE_INSENSITIVE_ORDER
field = static final byte java.lang.String.LATIN1
field.getType() = byte LATIN1
field = static final byte java.lang.String.UTF16
field.getType() = byte UTF16
method = byte[] java.lang.String.value()
method = public boolean java.lang.String.equals(java.lang.Object)
method = public int java.lang.String.length()
method = public java.lang.String java.lang.String.toString()
method = static void java.lang.String.checkIndex(int,int)
method = public int java.lang.String.hashCode()
method = public void java.lang.String.getChars(int,int,char[],int)
method = public int java.lang.String.compareTo(java.lang.Object)
method = public int java.lang.String.compareTo(java.lang.String)
method = public int java.lang.String.indexOf(java.lang.String,int,int)
method = static int java.lang.String.indexOf(byte[],byte,int,java.lang.String,int)
method = public int java.lang.String.indexOf(java.lang.String,int)
method = public int java.lang.String.indexOf(int)
method = public int java.lang.String.indexOf(int,int)
method = public int java.lang.String.indexOf(int,int,int)
method = public int java.lang.String.indexOf(java.lang.String)
method = public static java.lang.String java.lang.String.valueOf(long)
method = public static java.lang.String java.lang.String.valueOf(char[])
method = public static java.lang.String java.lang.String.valueOf(java.lang.Object)
method = public static java.lang.String java.lang.String.valueOf(char[],int,int)
method = public static java.lang.String java.lang.String.valueOf(float)
method = public static java.lang.String java.lang.String.valueOf(double)
method = public static java.lang.String java.lang.String.valueOf(char)
method = public static java.lang.String java.lang.String.valueOf(boolean)
method = public static java.lang.String java.lang.String.valueOf(int)
method = byte java.lang.String.coder()
method = private static java.lang.Void java.lang.String.rangeCheck(char[],int,int)
method = static int java.lang.String.checkBoundsOffCount(int,int,int)
method = private static java.nio.charset.Charset java.lang.String.lookupCharset(java.lang.String) throws java.io.UnsupportedEncodingException
method = private static char java.lang.String.decode2(int,int)
method = private static int java.lang.String.decodeUTF8_UTF16(byte[],int,int,byte[],int,boolean)
method = private static int java.lang.String.scale(int,float)
method = private static int java.lang.String.decodeWithDecoder(java.nio.charset.CharsetDecoder,char[],byte[],int,int) throws java.nio.charset.CharacterCodingException
method = private static java.lang.String java.lang.String.newStringNoRepl1(byte[],java.nio.charset.Charset)
method = static java.lang.String java.lang.String.newStringUTF8NoRepl(byte[],int,int,boolean)
method = private static void java.lang.String.throwMalformed(byte[])
method = private static void java.lang.String.throwMalformed(int,int)
method = private static byte[] java.lang.String.encodeUTF8(byte,byte[],boolean)
method = private static byte[] java.lang.String.encode8859_1(byte,byte[],boolean)
method = private static byte[] java.lang.String.encode8859_1(byte,byte[])
method = private static byte[] java.lang.String.encodeASCII(byte,byte[])
method = private static byte[] java.lang.String.encodeWithEncoder(java.nio.charset.Charset,byte,byte[],boolean)
method = private static byte[] java.lang.String.safeTrim(byte[],int,boolean)
method = private static byte[] java.lang.String.encode(java.nio.charset.Charset,byte,byte[])
method = private static byte[] java.lang.String.getBytesNoRepl1(java.lang.String,java.nio.charset.Charset)
method = private static boolean java.lang.String.isASCII(byte[])
method = private static void java.lang.String.throwUnmappable(byte[])
method = private static void java.lang.String.throwUnmappable(int)
method = private static void java.lang.String.replaceNegatives(byte[],int)
method = private static boolean java.lang.String.isNotContinuation(int)
method = private static boolean java.lang.String.isMalformed3(int,int,int)
method = private static int java.lang.String.malformed3(byte[],int)
method = private static char java.lang.String.decode3(int,int,int)
method = private static boolean java.lang.String.isMalformed3_2(int,int)
method = private static int java.lang.String.decode4(int,int,int,int)
method = private static boolean java.lang.String.isMalformed4(int,int,int)
method = private static int java.lang.String.malformed4(byte[],int)
method = private static boolean java.lang.String.isMalformed4_2(int,int)
method = private static boolean java.lang.String.isMalformed4_3(int)
method = private static byte[] java.lang.String.encodeUTF8_UTF16(byte[],boolean)
method = boolean java.lang.String.isLatin1()
method = public char java.lang.String.charAt(int)
method = public int java.lang.String.codePointAt(int)
method = public int java.lang.String.codePointBefore(int)
method = public int java.lang.String.codePointCount(int,int)
method = public int java.lang.String.offsetByCodePoints(int,int)
method = static void java.lang.String.checkBoundsBeginEnd(int,int,int)
method = public byte[] java.lang.String.getBytes()
method = void java.lang.String.getBytes(byte[],int,int,byte,int)
method = void java.lang.String.getBytes(byte[],int,byte)
method = public void java.lang.String.getBytes(int,int,byte[],int)
method = public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
method = public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
method = public boolean java.lang.String.contentEquals(java.lang.CharSequence)
method = public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
method = private boolean java.lang.String.nonSyncContentEquals(java.lang.AbstractStringBuilder)
method = public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
method = public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
method = public boolean java.lang.String.startsWith(java.lang.String)
method = public boolean java.lang.String.startsWith(java.lang.String,int)
method = static int java.lang.String.lastIndexOf(byte[],byte,int,java.lang.String,int)
method = public int java.lang.String.lastIndexOf(int)
method = public int java.lang.String.lastIndexOf(java.lang.String)
method = public int java.lang.String.lastIndexOf(int,int)
method = public int java.lang.String.lastIndexOf(java.lang.String,int)
method = public java.lang.String java.lang.String.substring(int,int)
method = public java.lang.String java.lang.String.substring(int)
method = public boolean java.lang.String.isEmpty()
method = public java.lang.String java.lang.String.replace(char,char)
method = public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
method = public boolean java.lang.String.matches(java.lang.String)
method = public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
method = public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
method = private java.lang.String[] java.lang.String.split(java.lang.String,int,boolean)
method = public java.lang.String[] java.lang.String.split(java.lang.String)
method = public java.lang.String[] java.lang.String.split(java.lang.String,int)
method = private java.lang.String[] java.lang.String.split(char,int,boolean)
method = public java.lang.String[] java.lang.String.splitWithDelimiters(java.lang.String,int)
method = static java.lang.String java.lang.String.join(java.lang.String,java.lang.String,java.lang.String,java.lang.String[],int)
method = public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.Iterable)
method = public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.CharSequence[])
method = public java.lang.String java.lang.String.toLowerCase()
method = public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
method = public java.lang.String java.lang.String.toUpperCase()
method = public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
method = public java.lang.String java.lang.String.trim()
method = public java.lang.String java.lang.String.strip()
method = public java.lang.String java.lang.String.stripLeading()
method = public java.lang.String java.lang.String.stripTrailing()
method = private int java.lang.String.indexOfNonWhitespace()
method = public java.util.stream.Stream java.lang.String.lines()
method = public java.lang.String java.lang.String.repeat(int)
method = private int java.lang.String.lastIndexOfNonWhitespace()
method = private static int java.lang.String.outdent(java.util.List)
method = public boolean java.lang.String.isBlank()
method = public char[] java.lang.String.toCharArray()
method = public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
method = public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
method = static void java.lang.String.repeatCopyRest(byte[],int,int,int)
method = public java.lang.Object java.lang.String.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup) throws java.lang.ReflectiveOperationException
method = public java.lang.String java.lang.String.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup)
method = public java.util.stream.IntStream java.lang.String.codePoints()
method = static java.lang.String java.lang.String.newStringNoRepl(byte[],java.nio.charset.Charset) throws java.nio.charset.CharacterCodingException
method = static byte[] java.lang.String.getBytesUTF8NoRepl(java.lang.String)
method = static byte[] java.lang.String.getBytesNoRepl(java.lang.String,java.nio.charset.Charset) throws java.nio.charset.CharacterCodingException
method = static int java.lang.String.decodeASCII(byte[],int,char[],int,int)
method = public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
method = public int java.lang.String.compareToIgnoreCase(java.lang.String)
method = public boolean java.lang.String.endsWith(java.lang.String)
method = public java.lang.CharSequence java.lang.String.subSequence(int,int)
method = public java.lang.String java.lang.String.concat(java.lang.String)
method = public boolean java.lang.String.contains(java.lang.CharSequence)
method = public java.lang.String java.lang.String.indent(int)
method = public java.lang.String java.lang.String.stripIndent()
method = public java.lang.String java.lang.String.translateEscapes()
method = public java.util.stream.IntStream java.lang.String.chars()
method = public java.lang.Object java.lang.String.transform(java.util.function.Function)
method = public java.lang.String java.lang.String.formatted(java.lang.Object[])
method = public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
method = public static java.lang.String java.lang.String.copyValueOf(char[])
method = public native java.lang.String java.lang.String.intern()
method = static void java.lang.String.checkOffset(int,int)
method = static java.lang.String java.lang.String.valueOfCodePoint(int)
method = public java.util.Optional java.lang.String.describeConstable()
method = private static java.lang.String java.lang.String.lambda$stripIndent$3(int,java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$2(int,java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$1(java.lang.String)
method = private static java.lang.String java.lang.String.lambda$indent$0(java.lang.String,java.lang.String)
Superclass: class java.lang.Object
Interface: java.io.Serializable
Interface: java.lang.Comparable
Interface: java.lang.CharSequence
Interface: java.lang.constant.Constable
Interface: java.lang.constant.ConstantDesc
Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다.
Class clazz = String.class; // 1. 클래스에서 조회.
Class clazz = new String().getClass(); // 2. 인스턴스에서 조회.
Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
Class 클래스의 주요 기능.
getDeclaredFields()
클래스의 모든 필드를 조회합니다.
getDeclaredMethods()
클래스의 모든 메서드를 조회합니다.
getSuperclass()
클래스의 부모 클래스를 조회합니다.
getInterfaces()
클래스의 인터페이스들을 조회합니다.
2️⃣ 클래스 생성하기.
Class 클래스에는 클래스의 모든 정보가 들어있습니다.
이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다.
아래의 예제에서는 간단하게 인스턴스를 생성해보겠습니다.
package langReview.clazz;
public class Hello {
public String hello() {
return "hello!";
}
}
package langReview.clazz;
public class ClassCreateMain {
public static void main(String[] args) throws Exception {
Class helloClass = Hello.class;
//Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
실행 결과
result = hello!
getDeclaredConstructor().newInstance()
getDeclaredConstructor() : 생성자를 선택합니다.
newInstance() : 선택된 생성자를 기반으로 인스턴스를 생성합니다.
리플랙션 - reflection
Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다.
이런 작업을 리플랙션이라고 합니다.
추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수 있습니다.
최신 프레임워크들은 이런 기능을 적극 활용합니다.
-
💾 [CS] MVC 패턴.
💾 [CS] MVC 패턴.
1️⃣ MVC 패턴.
MVC 패턴은 모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴입니다.
애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발할 수 있습니다.
재사용성과 확장성이 용이하다는 장점이 있고, 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해지는 단점이 있습니다.
Model(모델)
모델(model)은 애플리케이션의 데이터인 데이터베이스, 상수, 변수 등을 뜻합니다.
예를 들어 사각형 모양의 박스 안에 글자가 들어 있다면 그 사각형 모양의 박스 위치 정보, 글자 내용, 글자 위치, 글자 포맷(utf-8 등)에 관한 정보를 모두 가지고 있어야 합니다.
뷰에서 데이터를 생성하거나 수정하면 컨트롤러를 통해 모델을 생성하거나 갱신합니다.
View(뷰)
뷰(View)는 Inputbox, checkbox, textarea 등 사용자 인터페이스 요소를 나타냅니다.
즉, 모델을 기반으로 사용자가 볼 수 있는 화면을 뜻합니다.
모델이 가지고 있는 정보를 따로 저장하지 않아야 하며 단순히 사각형 모양 등 화면에 표시하는 정보만 가지고 있어야 합니다.
또한, 변경이 일어나면 컨트롤러에 이를 전달해야 합니다.
Controller(컨트롤러)
컨트롤러(Controller)는 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 하며 이벤트 등 메인 로직을 담당합니다.
또한, 모델과 뷰의 생명주기도 관리하며, 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용에 대해 알려줍니다.
2️⃣ MVC 패턴의 예 리액트.
MVC 패턴을 이용한 대표적인 프레임워크로는 자바 플랫폼을 위한 오픈 소스 애플리케이션 프레임워크인 스프링(Spring)이 있습니다.
Spring의 WEB MVC는 웹 서비스를 구축하는 데 편리한 기능들을 많이 제공합니다.
예를 들어 @RequestParam, @RequestHaader, @PathVariable 등의 애너테이션을 기반으로 사용자의 요청 값들을 쉽게 분석할 수 있으며 사용자의 어떠한 요청이 유효한 요청인지 쉽게 거를 수 있습니다.
예를 들어 숫자를 입력해야 하는데 문자를 입력하는 사례 같은 것 말이죠.
또한 재사용 가능한 코드, 테스트, 쉽게 리디렉션할 수 있게 하는 등의 장점이 있습니다.
3️⃣ 자바에서의 MVC 패턴.
자바에서의 MVC 패턴(Model-View-Contorller) 은 웹 애플리케이션 개발에서 널리 사용되는 소프트웨어 디자인 패턴입니다.
이 패턴은 애플리케이션을 모델(Model), 뷰(View), 컨트롤러(Controller)로 분리하여 코드의 유지보수성과 확장성을 높이는 구조를 제공합니다.
MVC 패턴의 구성 요소.
1. Model(모델)
역할 : 애플리케이션의 데이터와 비즈니스 로직 을 처리합니다.
기능 :
데이터베이스와의 상호작용.
데이터 저장, 수정, 삭제와 같은 비즈니스 로직 처리.
데이터를 가공하여 제공.
예 : 데이터베이스 엔티티, DAO, 서비스 클래스.
예시 코드 :
public class User {
private String username;
private String email;
// Getter and Setter
}
2. View(뷰)
역할 : 사용자가 보는 UI(사용자 인터페이스) 를 담당합니다. 모델로부터 데이터를 받아와서 사용자에게 보여줍니다.
기능 :
HTML, JSP, Thymeleaf 같은 템플릿 엔진을 사용하여 사용자에게 데이터를 렌더링
데이터 입력, 출력 및 이벤트 처리.
예 : JSP, Thymeleaf, HTML 파일.
예시코드(Thymeleaf)
```java
User List
#### 3. Controller(컨트롤러)
- **역할 :** 사용자의 요청을 처리하고, 필요한 데이터를 모델에서 가져와서 뷰에 전달하는 역할을 합니다.
- **기능 :**
- 사용자 입력을 받고, 이를 처리할 적절한 로직(모델)으로 전달
- 모델로부터 데이터를 받아서 적절한 뷰로 반환.
- HTTP 요청을 처리하고, 결과를 뷰에 반영.
- **예 :** Spring MVC의 `@Controller` 클래스.
- **예시코드 (Spring Boot)**
```java
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public String listUsers(Model model) {
List<User> users = userService.getAllUsers();
model.addAttribute("users", users);
return "userList"; // userList.html로 반환
}
}
MVC 패턴의 흐름.
1. 사용자의 요청
사용자가 브라우저에서 URL을 입력하거나 버튼을 클릭하는 등의 동작을 통해 컨트롤러로 요청이 전달됩니다.
2. 컨트롤러의 처리
컨트롤러는 사용자의 요청을 받고, 비즈니스 로직이 필요한 경우 모델을 호출하여 데이터를 처리하거나 가져옵니다.
3. 모델의 처리
모델은 데이터베이스와 상호작용하여 데이터를 읽고, 수정하거나 추가/삭제한 후, 컨트롤러로 결과를 반환합니다.
4. 뷰에 데이터 전달
컨트롤러는 모델에서 받은 데이터를 뷰로 전달하고, 해당 뷰가 사용자에게 보여지도록 응답을 생성합니다.
5. 결과 반환
최종적으로 사용자는 브라우저에서 컨트롤러가 처리한 결과를 볼 수 있습니다.
자바에서 MVC 패턴을 사용하는 예.
자바에서는 Spring MVC 프레임워크를 사용하요 MVC 패턴을 구현하는 것이 일반적입니다.
Spring MVC는 컨트롤러, 모델, 뷰의 역할을 분리하여 웹 애플리케이션을 개발할 수 있게 해줍니다.
Spring MVC의 기본 흐름.
1. DispatcherServler
모든 요청은 먼저 DispatcherServlet으로 전달됩니다.
이것은 Front Controller로서, 요청을 적절한 컨트롤러로 라우팅합니다.
2. Controller
DispatcherServler은 요청을 처리할 적절한 컨트롤러 메서드를 호출합니다.
3. Model
컨트롤러는 필요한 경우 모델과 상호작용하여 데이터를 가져오거나 처리합니다.
4. View
컨트롤러는 모델에서 처리된 데이터를 뷰에 전달합니다.
5. View Resolver
뷰 리졸버(View Resolver)가 HTML, JSP, Thymeleaf 템플릿 등과 같은 뷰를 렌더링하여 클라이언트에게 응답을 보냅니다.
Spring MVC 코드 예시.
1. Controller
@Controller
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/products")
public String getAllProducts(Model model) {
List<Product> products = productService.getAllProducts();
model.addAttribute("products", products);
return "productList"; // productList.html로 반환
}
}
2. Model(Service & Entity)
```java
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public List getAllProducts() {
return productRepository.findAll();
}
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// Getter and Setters } ```
3. View(Thymeleaf 템플릿)
```html
<!DOCTYPE html>
Product List
Product List
Product Name -
Price
```
MVC 패턴의 장점.
1. 유지보수성 향상.
비즈니스 로직(Model)과 사용자 인터페이스(View)를 분리함으로써 코드를 쉽게 유지보수할 수 있습니다.
UI를 수정해도 비즈니스 로직에는 영향을 미치지 않습니다.
2. 확장성.
각 부분(Model, View, Controller)을 독립적으로 확장할 수 있어 확장성이 뛰어납니다.
3. 테스트 용이성.
비즈니스 로직과 UI가 분리되어 있어, 각각의 부분을 독립적으로 테스트할 수 있습니다.
4️⃣ 결론.
MVC 패턴은 자바 기반 웹 애플리케이션에서 중요한 디자인 패턴으로, 애플리케이션을 모델, 뷰, 컨트롤러로 분리하여 구조를 명확하게 유지하고 코드의 재사용성을 높이는 데 기여합니다.
Spring MVC는 이를 자바에서 쉽게 구현할 수 있는 대표적인 프레임워크로, 많은 자바 개발자들이 사용하는 방식입니다.
-
-
🌐[Network] OSI 7계층 모델 - 계층별 기능
🌐[Network] OSI 7계층 모델 - 계층별 기능.
1️⃣ 계층별 기능.
OSI 7계층 모델의 모든 계층이 중요하지만, 특히 전송 계층이 중요합니다.
전송 계층은 통신 양단에 있는 전송 연결의 주체(프로세스) 사이에 종단 연결을 제공합니다.
호스트에서 실행되는 프로세스와 프로세스 사이에 연결을 설정하여 데이터를 주고받을 수 있게 해주는 것이 전송 계층입니다.
전화 시스템을 예로 들면, 통화자 사이에 통화 연결을 설정하는 것이 바로 전송 계층입니다.
전송 계층의 하위에 있는 물리 계층, 데이터 링크 계층, 네트워크 계층은 전송 계층의 연결을 설정하고 지원하는 역할을 수행합니다.
상위에 있는 세션 계층, 표현 계층 응용 계층은 전송 계층의 연결을 어떻게 활용할지에 대하여 다룹니다.
따라서 OSI 7계층 모델은 전송 계층을 기준으로 두 부분으로 나뉜다고 볼 수 있습니다.
실제로 인터넷 모델인 TCP/IP 프로토콜은 운영체제 내부에 계층 4까지의 기능을 구현하고, 상위 계층의 기능은 사용자 프로그램으로 구현합니다.
1️⃣ 물리 계층.
OSI 7계층 모델의 맨 밑에 위치하는 물리 계층(Physical Layer)은 전송 매체의 물리적 인터페이스에 관한 사항을 기술합니다.
즉, 전송 매체에서는 개별 정보의 비트(Bit) 교환 문제를 다룹니다.
물리 계층은 하드웨어 시스템으로 구현되고, 계층 2이상의 프로토콜들은 소프트웨어적으로 구현됩니다.
물리 계층에서 다루는 전송 매체의 특성에는 데이터의 전송 속도, 송수신 호스트 사이의 클록 동기화 방법, 물리적 연결 형태 등이 있습니다.
2️⃣ 데이터 링크 계층.
데이터 링크 계층(Data Link Layer)은 물리 계층을 통해 전송되는 데이터의 물리적 전송 오류를 해결합니다.
결과적으로 상위의 네트워크 계층에 신뢰성 있는 패킷 전송을 보장해주어 물리적 전송 오류에 대한 부담을 없애줍니다.
데이터 링크 계층은 갈림길에서 전송 경로를 선택할 수 없으므로 두 호스트가 일대일로 직접 연결된 환경에서만 데이터 전송을 지원합니다.
데이터 링크 계층을 이용해 전송되는 데이터를 프레임(Frame)이라 부릅니다.
프레임 헤더에 표시되는 송수신 호스 정보에는 LAN 카드에 내장된 송수신 호스트의 MAC 주소가 기록됩니다.
데이터 링크 계층은 다른 상위 계층처럼 송신 호스트와 수신 호스트 사이의 전송 속도 차이를 고려한 흐름 제어 기능도 지원할 수 있습니다.
3️⃣ 네트워크 계층.
네트워크 계층(Network Layer)은 송신 호스트가 전송한 데이터가 어떤 경로를 통해 수신 호스트에 전달되는지를 결정하는 라우팅 문제를 처리합니다.
전달 경로 선택은 미리 정해지는 정적인(static) 방식과 네트워크의 현재 부하 상태에 따라 결정되는 동적인(Dynamic) 방식으로 구분합니다.
네트워크 계층에서는 전송 데이터 패킷(Packet)이라 부르며, 중개 과정에서 경로 선택의 기준이 되는 호스트 주소가 필요합니다.
인터넷에서는 IP 프로토콜이 네트워크 계층의 기능을 수행하므로 호스트의 IP 주소가 경로 선택에 중요한 기준이 됩니다.
인터넷에 연결된 호스트는 네트워크 계층의 주소와 데이터 링크 계층의 주소를 모두 가집니다.
컴퓨터 네트워크를 이용해 전송되는 패킷이 지나치게 많으면 네트워크의 전송 속도가 떨어집니다.
네트워크의 전송 속도가 감소하면 프로토콜 동작에 많은 영향을 미칠 수 있는데, 네트워크 트래픽이 과도하게 증가하는 문제를 조절하는 혼잡 제어(Congestion Control) 기능도 네트워크 계층에서 담당합니다.
4️⃣ 전송 계층.
전송 계층(Transport Layer)은 송신 프로세스와 수신 프로세스를 직접 연결하는 단대단(End-to-End) 통신 기능을 제공합니다.
전송 계층 아래에 있는 하위 계층은 호스트와 호스트 사이의 데이터 전송 과정에서 발생하는 문제들만 반영합니다.
반면 전송 계층은 호스트 내부에서 논리적으로 구축되는 연결 주체인 프로세스 사이의 통신 문제를 다룹니다.
전송 계층에서는 전송 오류율, 전송 속도 등과 같은 일반 사용자의 서비스 요구 유형에 대한 고려와 흐름 제어 기능도 제공합니다.
5️⃣ 세션 계층.
세션 계층(Session Layer)의 기능은 전송 계층과 거의 유사합니다.
그러나 사용자에게 원격 파일 전송이나 원격 로그인 등과 같은 상위적 연결 개념인 세션 기능을 제공한다는 점이 다릅니다.
특히 전송 계층의 연결이 일시적으로 끊어 졌을 때 이를 복구하여 세션이 유지될 수 있도록 합니다.
세션 계층에서는 송수신 호스트 사이의 대화 제어를 비롯해 상호 배타적인 동작을 제어하기 위한 토큰 제어, 일시적인 전송 장애를 해결하기 위한 동기(Synchronization) 기능 등을 제공합니다.
6️⃣ 표현 계층.
계층 5까지는 주로 데이터의 전송에 관한 내용을 다루지만 표현 계층(Presentation Layer)은 데이터의 의미(Sementic)와 표현 방법(Syntax)을 처리합니다.
즉, 통신 양단에서 서로 이해할 수 있는 표준 방식으로 데이터를 코딩(Coding)하는 문제를 다룹니다.
호스트의 데이터 표현 방법이 서로 다르면 상대방의 데이터를 이해할 수 있도록 적절하게 변환하는 과정이 필요합니다.
인터넷상에서 개인 정보의 유통과 상거래가 활발해지면서 보안의 중요성이 강조되고 있는데, 데이터를 암호화하는 기술도 표현 계층에서 다룹니다.
또한 영상 정보 같은 대용량 데이터의 크기를 줄여주는 압축도 표현 계층의 주요 기능입니다.
7️⃣ 응용 계층.
최상위의 응용 계층(Application Layer)에서는 다양하게 존재하는 응용 환경에서 필요한 기능을 다룹니다.
응용 환경은 매우 다양해 범위가 방대하지만, 초기 인터넷에서는 FTP, 텔넷, 전자 메일 서비스가 보급되었습니다.
FTP(File Transfer Protocol)는 파일 공유 서비스이며, 이 기능이 발전되어 지금은 클라우드 서비스가 보편적으로 사용되고 있습니다.
네트워크 파일을 이용할 때는 파일을 공유할 뿐 아니라 허가된 사용자가 적절한 접근 권한에 따라 사용할 수 있게 해야 합니다.
텔넷(Telnet)이 제공하는 가상 터미널, 전자 메일 등도 대표적인 초기 인터넷 서비스였습니다.
FTP와 Telnet은 최근 일반 사용자들 사이에서는 많이 사용되지 않고 있습니다.
그에 비하여 메일 서비스는 현재까지도 가장 많이 사용되는 인터넷 서비스 중 하나입니다.
현재의 인터넷 환경을 보면 스마트폰의 보급과 인공지능 기술의 발전으로 응용 계층의 범위가 폭넓게 확산되는 추세입니다.
-
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능.
1️⃣ 주요 메서드.
래퍼 클래스가 제공하는 주요 메서드를 알아봅시다.
public class WrapperUtilsMain {
public static void main(String[] args) {
Integer i1 = Integer.valueOf(10); // 숫자를 래퍼 객체로 변환.
Integer i2 = Integer.valueOf("10");// 문자열을 래퍼 객체로 변환.
int intValue = Integer.parseInt("10");// 문자열 전용 기능, 기본형으로 변환.
// 비교
int compareResult = i1.compareTo(20);
System.out.println("compareResult = " + compareResult);
// 산술 연산
System.out.println("sum: " + Integer.sum(10, 20));
System.out.println("min: " + Integer.min(10, 20));
System.out.println("max: " + Integer.max(10, 20));
}
}
실행 결과
compareResult = -1
sum: 30
min: 10
max: 20
valueOf() : 래퍼 타입을 반환합니다. 숫자, 문자열을 모두 지원합니다.
parseInt() : 문자열을 기본형으로 변환합니다.
compareTo() : 내 값과 인수로 넘어온 값을 비교합니다. 내 값이 크면 1, 같으면 0, 내 값이 작으면 -1을 반환합니다.
Integer.sum(), Integer.min(), Integer.max() : static 메서드입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다.
parseInt()와 valueOf()
언제 parseInt()를 사용하고 valueOf()를 사용해야 할까요?
원하는 타입에 맞는 메서드를 사용하면 됩니다.
valueOf("10") 는 래퍼 타입을 반환합니다.
parseInt("10") 는 기본형을 반환합니다.
Long.parseLong() 처럼 각 타입에 parseXxx() 가 존재합니다.
2️⃣ 래퍼 클래스와 성능.
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.
그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?
다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다.
public class WrapperVsPrimitive {
public static void main(String[] args) {
int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
long startTime, endTime;
// 기본형 long 사용
long sumPrimitive = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumPrimitive += i;
}
endTime = System.currentTimeMillis();
System.out.println("sumPrimitive = " + sumPrimitive);
System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
// 래퍼 클래스 Long 사용
Long sumWrapper = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumWrapper += i; // 오토 박싱 발생
}
endTime = System.currentTimeMillis();
System.out.println("sumWrapper = " + sumWrapper);
System.out.println("기본 자료형 Long 실행 시간: " + (endTime - startTime) + "ms");
}
}
실행 결과 - M1 맥북 기준
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 346ms
sumWrapper = 499999999500000000
기본 자료형 Long 실행 시간: 1555ms
기본형 연산이 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다.
참고로 계산 결과는 시스템 마다 다릅니다.
기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다.
예를 들어 int는 보통 4바이트의 메모리를 사용합니다.
래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다.
자바 버전과 시스템마다 다르지만 대략 8~16바이트의 메모리를 추가로 사용합니다.
기본형, 래퍼 클래스 어떤 것을 사용해야 할까?
이 연산은 10억 번의 연산을 수행했을 때 0.3초와 1.5초의 차이입니다.
기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘 다 매우 빠르게 연산이 수행됩니다.
0.3초 나누기 10억, 1.5초 나누기 10억입니다.
일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다.
CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려해야합니다.
그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다.
유지보수 vs 최적화.
유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다.
특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다.
코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다.
최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다.
그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다.
특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다.
자바 메모리 내부에서 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다.
권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
-
💾[Database] 데이터베이스 모델링.
💾[Database] 데이터베이스 모델링.
1️⃣ 정보 시스템 구축의 개요.
정보 시스템을 구축하기 위해서는 일반적으로 “분석-설계-구현-시험-유지.보수”라는 5단계를 거칩니다.
분석은 프로젝트의 첫 번째 단계로 시스템 분석 또는 요구 사항 분석이라고도 합니다.
요구 사항 분석은 ‘무엇을(what)’할지 결정하는 것으로, 이 단계에는 사용자 인터뷰와 업무 조사 등을 수행합니다.
분석은 프로젝트의 첫 단추를 끼우는 중요한 단계이므로 당연히 많은 시간 동안 심혈을 기울여야 합니다.
두 번째로 설계 단계는 시스템 설계 또는 프로그램 설계라고도 하며, 구축하고자 하는 시스템을 ‘어떻게(how)’ 설계할 것인지 결정합니다.
설계 단계가 끝나면 그 결과 문서를 프로그래머(또는 코더)에게 넘겨주고, 프로그래머는 설계서에 있는 대로 프로그램을 작성하기만 하면 됩니다.
따라서 일반적으로는 시스템 설계가 끝나면 가장 큰 작업을 마친 것으로 여깁니다.
대부분의 프로젝트에서는 분석과 설계 단계가 전체 공정의 50% 이상을 차지합니다.
2️⃣ 데이터베이스 모델링과 필수 용어.
데이터베이스 모델링이란 현실 세계에서 사용되는 데이터를 MySQL에 어떻게 옮겨놓을지 결정하는 과정으로, 정보 시스템 구축 시 분석과 설계 단계에서 가장 중요한 작업 중 하나입니다.
인터넷 쇼핑몰을 생각해봅시다.
인터넷 쇼핑몰에는 사람(또는 회원)이 필요합니다.
그렇다면 이 사람을 어떻게 MySQL에 넣을까요?
데이터베이스에서는 사람을 나타내는 여러 가지 특성(속성)을 추출하여 저장합니다.
어떤 사람의 신분을 증명하는 신분증에 이름, 주민등록번호, 주소 등의 정보가 있는 것과 비슷한 개념입니다.
일상 생활에서 판매하는 제품도 마찬가지입니다.
제품 자체를 컴퓨터에 넣을 수는 없으니 제품명, 가격, 제조일, 제조사, 남은 수량 등을 저장합니다.
이때 정보가 단편적으로 저장되는 것이 아니라 테이블이라는 형식에 맞춰 저장됩니다.
지금까지 설명한 사람과 제품에 관한 정보를 테이블에 구현하면 다음 그림과 같습니다.
테이블의 구조와 관련 용어.
데이터.
토마스, 컴퓨터, 2019-07-01과 같이 테이블에 저장된 하나하나의 단편적인 정보를 데이터라고 합니다.
즉 정보는 있으나 아직 체계화되지 않은 상태입니다.
테이블.
회원이나 제품의 데이터를 입력하기 위해 표 형태로 만든 것을 말합니다.
위 그림에서는 인터넷 쇼핑몰을 구현하기 위해 회원 정보를 보관할 회원 테이블과 제품 정보를 보관할 제품 테이블을 합쳐서 총 2개의 테이블을 만들었습니다.
데이터베이스.
테이블이 저장되는 저장소로, 위 그림과 같이 원통 모양으로 표현합니다.
위 그림에는 3개의 데이터베이스가 있으며, 각 데이터베이스는 각각의 고유한 이름을 가지고 있습니다.
DBMS.
DataBase Management System의 약자로, 데이터베이스를 관리하는 시스템 또는 소프트웨어를 말합니다.
예를 들어 MySQL과 같은 것이 DBMS이다.
위 그림에서는 DBMS가 3개의 데이터베이스를 관리하고 있습니다.
열(필드).
각 테이블은 열(Column)로, 구성됩니다.
위 그림의 회원 테이블은 ‘아이디’, ‘회원 이름’, ‘주소’라는 3개의 열로 구성되어 있습니다.
데이터 형식.
열의 데이터 형식을 말합니다.
회원 테이블의 회원 이름 열은 당연히 숫자가 아닌 문자 형식이어야 합니다.
또한 제품 테이블의 가격 열은 숫자(특히 정수) 형식이어야 합니다.
가격에 ‘고가’같은 글자가 들어가서는 안 되기 때문입니다.
데이터 형식은 테이블을 생성할 때 열 이름과 함꼐 지정합니다.
행(레코드).
실질적인 데이터를 말합니다.
회원 테이블의 ‘Thomas/토마스/경기도 부천시 중동’은 하나의 행으로 ‘행 데이터’라고도 부릅니다.
행(Row)은 회원 테이블에서 회원이 몇 명인지, 행 데이터가 몇 개 있는지와 동일한 의미입니다.
회원 테이블의 행은 4개, 즉 4명의 회원이 존재합니다.
기본키(주키).
기본키는 각 행을 구분하는 유일한 열을 말합니다.
기본키는 중복되면 안 되고 비어 있어서도 안 됩니다.
또한 각 테이블에는 기본키가 하나만 지정되어 있어야 합니다.
회원 테이블의 기본키는 아이디 열에 지정되어 있는데, 만약 기본키를 회원 이름 열에 지정하면 어떻게 될까요?
기본키는 각 행을 구분하는 유일한 열이라고 했는데, ‘토마스’라는 이름만으로 그 사람이 경기도 부천시 중동에 산다는 것을 확신할 수 있나요?
만약 ‘토마스’라는 이름이 또 있다면?
현실적으로 같은 이름을 가진 사람이 있을 수 있기 때문에 회원 이름 열은 기본키로 지정하기에 부적합합니다.
그렇다면 주소 열은 어떨까요?
마찬가지로 주소만 가지고 유일한 사람이라고 단정지을 수 없습니다.
같은 집에 여러 사람이 살 수도 있기 때문입니다.
마지막의 아이디 열은 어떤가요?
쇼핑몰 사이트에 가입해봤다면 회원 가입을 할 때 아이디를 만들면서 버튼을 클릭해본 경험이 있을 것 입니다.
즉 아이디는 중복되지 않게 지정해야 합니다.
또한 쇼핑몰 사이트에 회원 가입을 할 때 아이디를 만들지 않고는 가입할 수 없습니다.
결국 모든 회원은 아이디를 가지고 있고 모든 회원의 아이디가 각각 다르기 때문에 아이디는 기본키로 지정하기에 매우 적절합니다.
한편 회원 테이블에 주민등록번호나 이메일 열이 있다면 역시 중복되지 않고 비어있지도 않으므로 기본키로 지정할 수 있습니다.
외래키
위 그림에서는 나타나 있지 않지만 외래키는 두 테이블의 관계를 맺어주는 키를 말합니다.
SQL(구조화된 질의 언어)
DBMS에서 어떤 작업을 하고 싶다면 어떻게 해야 할까요?
사람끼리 주고 받는 언어로 말할 수는 없습니다.
DBMS에서 어떤 작업을 하고 싶다면 DBMS가 알아듣는 말로 해야 할 텐데, 이때 사용하는 것이 바로 SQL입니다.
SQL은 사람과 DBMS가 소통하기 위한 말(언어)입니다.
-
-
💾[Database] DBMS의 개요.
💾[Database] DBMS의 개요.
1️⃣ 데이터베이스의 정의와 특징.
데이터베이스는 여러 사용자나 응용 프로그램이 공유하고 동시에 접근 가능한 ‘데이터의 집합’이라고 정의할 수 있습니다.
그리고 DBMS(DataBase Management System)는 이러한 데이터베이스를 관리.운영하는 소프트웨어입니다.
일반적으로 사용하는 MS의 엑셀 같은 프로그램은 데이터의 집합을 다루기 때문에 DBMS와 비슷해 보이지만, 대용량 데이터를 관리하거나 여러 명의 사용자가 공유하는 것은 아니므로 DBMS라고 부르지 않습니다.
데이터베이스는 ‘데이터의 저장 공간’ 자체를 의미하기도 합니다.
MySQL에서는 데이터베이스를 자료가 저장되는 디스크 공간(주로 파일로 구성됨)으로 취급합니다.
DBMS는 데이터베이스를 관리하는 역할을 하는 소프트웨어입니다.
여러 사용자나 응용 프로그램은 DBMS가 관리하는 데이터에 동시에 접속하여 데이터를 공유합니다.
즉, DBMS에서는 데이터베이스에서 사용되는 데이터가 집중 관리됩니다.
DBMS 또는 데이터베이스의 중요한 특징.
데이터 무결성.
데이터베이스 안의 데이터는 어떤 경로를 통해 들어왔든 오류가 있어서는 안 되는데 이를 무결성(Integrity)이라고 합니다.
무결성을 지키기 위해 데이터베이스는 제약 조건(constraint)을 따릅니다.
예를 들어 학생 데이터에서 모든 학생은 학번이 반드시 있어야 하고 학번이 중복되면 안 된다는 제약 조건을 생각해봅시다.
이 제약 조건을 충실히 지킨다면 학번으로도 학생 데이터에서 학생을 정확히 찾을 수 있습니다.
즉, 학번은 무결한 데이터를 보장하는 요소이며, 자동 발급기로 성적 증명서나 재학 증명서를 뗄 떼 학번만 조회해도 정확한 자료를 출력할 수 있습니다.
데이터의 독립성.
데이터베이스의 크기를 변경하거나 데이터 파일의 저장소를 변경하더라도 기존에 작성된 응용 프로그램은 전혀 영향을 받지 않습니다.
즉 데이터베이스와 응용 프로그램은 서로 의존적인 관계가 아니라 독립적인 관계입니다.
예를 들어 데이터베이스가 저장된 디스크가 새것으로 변경되어도 기존에 사용하던 응용 프로그램은 아무런 변경 없이 계속 사용할 수 있습니다.
보안.
데이터베이스 안의 데이터는 아무나 접근할 수 있는 것이 아니라 데이터를 소유한 사람이나 데이터에 접근이 허가된 사람만 접근할 수 있습니다.
또한 같은 데이터에 접근할 때도 사용자의 계정에 따라서 각각 다른 권한을 갖습니다.
최근 들어 고객 정보 유출 사고가 빈번하여 보안(Security)은 데이터베이스에서 더욱 중요한 이슈가 되고 있습니다.
데이터 중복 최소화.
데이터베이스에서는 동일한 데이터가 여러 군데 중복 저장되는 것을 방지합니다.
학교를 예로 들면, 학생 정보를 이용하는 교직원들(학생처, 교무처, 과사무실 등)이 각 직원마다 별도의 엑셀 파일로 학생 정보를 관리하면 한 명의 학생 정보가 각각의 엑셀 파일에 중복 저장됩니다.
그러나 데이터베이스에 통합하여 관리하면 하나의 테이블에 데이터를 저장한 후 응용 프로그램마다 이를 공유하여 사용할 수 있어 데이터의 중복을 최소화할 수 있습니다.
응용 프로그램 제장 및 수정 용이.
기존 파일 시스템에서는 각각의 파일 포맷에 맞춰 응용 프로그램을 개발했습니다.
그러나 데이터베이스를 이용하면 통일된 방식으로 응용 프로그램을 작성할 수 있고 유지.보수 또한 쉽습니다.
데이터의 안전성 향상.
대부분의 DBMS는 데이터 백업/복원 기능을 제공합니다.
따라서 데이터가 손상되는 문제가 발생하더라도 원래의 상태로 복원 또는 복구할 수 있습니다.
2️⃣ DBMS의 분류.
DBMS는 크게 계층형(Hierarchical), 망형(network), 관계형(relational), 객체지향형(object-oriented), 객체관계형(object-relational)으로 분류됩니다.
현재는 관계형 DBMS(Relational DBMS)가 가장 많이 사용되며, 일부 멅티미디어 분야에서는 객체지향형(object-oriented)나 객체관계형(object-relational) DBMS가 활용되기도 합니다.
MySQL을 비롯해 Oracle, DB2, SQL Server, Access 등은 모두 관계형 DBMS입니다.
계층형 DBMS(Hierarchical DBMS)
1960년대에 처음 등장한 DBMS 개념입니다.
위 그림에서 보듯이 각 계층이 트리 형태를 띠고 1:N 관계를 갖습니다.
예를 들어 사장 1명에 부서 3개가 연결되어 있는 구조가 계층형 구조입니다.
계층형 DBMS는 구축한 후 구조를 변경하기가 상당히 까다롭고, 주어진 상태에서 검색은 빠르지만 접근의 유연성이 부족하여 임의 검색 시 어려움이 있는 것이 단점입니다.
망형 DBMS(Network DBMS)
망형 DBMS는 계층형 DBMS의 문제점을 개선하기 위해 1970년대에 시작되었습니다.
망형 DBMS에서는 1:1, 1:N, N:M(다대다) 관계가 지원되어 효과적이고 빠른 데이터 추출이 가능합니다.
그러나 계층형 DBMS와 마찬가지로 매우 복잡한 내부 포인터를 사용하고 프로그래머가 모든 구조를 이해해야만 프로그램을 작성할 수 있다는 단점이 여전히 존재합니다.
관계형 DBMS
관계형 DBMS(Relational DBMS)는 1969년 에드거 F.코드(Edgar F.Codd)가 수학 모델에 근거하여 고안했습니다.
관계형 DBMS의 핵심개념은 ‘데이터베이스는 테이블(table)이라는 최소 단위로 구성되어 있으며, 이 테이블은 하나 이상의 열(Column)로 구성되어 있다’는 것입니다.
관계형 DBMS에서는 모든 데이터가 테이블에 저장됩니다.
테이블이라는 구조는 관계형 DBMS의 가장 기본적이고 중요한 구성으로, 테이블을 잘 이해하면 관계형 DBMS의 기본적인 것을 이해했다고 말할 수 있습니다.
테이블은 데이터를 효율적으로 저장하기 위한 구조입니다.
관계형 DBMS에서는 데이터를 하나가 아닌 여러 개의 테이블에 나누어 저장하므로 불필요한 공간의 낭비를 줄이고 데이터 저장의 효율성을 보장합니다.
이렇게 나뉜 테이블의 관계를 기본키(Primary Key, PK)와 외래키(Foreign Key, FK)를 사용하여 맺음으로써 두 테이블을 부모와 자식 관계로 묶습니다.
그리고 부모와 자식 관계로 연결된 테이블을 서로 조합하여 원하는 결과를 얻을 수 있는데, 이때 SQL(Structured Query Language, 구조화된 질의 언어)의 조인(join) 기능을 이용합니다.
TIP: 테이블은 릴레이션(Relation), 엔티티(Entity) 등으로 불립니다.
관계형 DBMS는 다른 DBMS에 비해 업무 변화에 따라 바로 순응할 수 있고 유지.보수 측면에서도 편리하다는 특징이 있습니다.
또한 대용량 데이터를 체계적으로 관리할 수 있고 데이터의 무결성도 잘 보장됩니다.
따라서 동시에 접근하는 여러 응용 프로그램을 사용할 때 관계형 DBMS는 적절한 선택이 될 수 있습니다.
관계형 DBMS의 단점으로는 시스템 자원을 많이 차지하여 시스템이 전반적으로 느려진다는 것을 꼽을 수 있습니다.
그러나 최근에는 하드웨어의 급속한 발전으로 이러한 단점이 많이 보완되고 있습니다.
3️⃣ SQL의 개요
SQL은 관계형 데이터베이스에서 사용되는 언어로 ‘에스큐엘’ 또는 ‘시퀄’이라고 읽습니다.
관계형 DBMS(그중에서도 MySQL)를 배우려면 SQL을 익히는 것이 필수입니다.
SQL은 데이터베이스를 조작하는 ‘언어’로, 일반적인 프로그래밍 언어(C, C++, Java, C# 등)와 다른 특성을 가지고 있다.
SQL의 특징.
DBMS 제작 회사와 독립적입니다.
모든 DBMS 제작 회사에 표준 SQL이 공개되어 각 회사는 이 표준 SQL에 맞춰 DBMS를 개발합니다.
따라서 SQL은 대부분의 DBMS 제품에서 공통적으로 호환됩니다.
다른 시스템으로의 이식성이 좋습니다.
SQL은 서버용, 개인용, 휴대용 장비 등 운영되는 DBMS마다 상호 호환성이 뛰어납니다.
한 시스템에서 사용하던 SQL을 다른 시스템으로 이식하는 데 큰 문제가 없습니다.
표준이 계속 발전합니다.
SQL은 SQL-86, SQL-89, SQL-92, SQL:1999, SQL:2003, SQL:2008, SQL:2011 등으로 개선된 표준안이 계속 발표되었으며, 지금도 개선된 안이 꾸준히 연구되고 있습니다.
대화식 언어입니다.
기존 프로그래밍 언어는 프로그램 작성, 컴파일 및 디버깅, 실행 과정을 거쳐야만 그 결과를 확인할 수 있지만 SQL은 바로 질의하고 결과를 얻는 대화식 언어입니다.
클라이언트/서버 구조를 지원합니다.
SQL은 분산형 구조인 클라이언트/서버 구조를 지원합니다.
클라이언트에서 질의를 하면 서버에서 그 질의를 받아 처리하여 클라이언트에 전달하는 구조입니다.
SQL을 사용할 때 주의할 점은, 모든 DBMS 제품의 SQL 문이 완벽하게 동일하지는 않다는 것입니다.
많은 회사가 되도록 표준 SQL을 준수하려고 노력하지만 각 회사의 DBMS마다 특징이 있기 때문에 현실적으로 완전히 통일되기는 어렵습니다.
각 회사는 가급적 표준 SQL을 지키면서도 자신의 제춤에 특화된 SQL을 사용합니다.
이를 오라큰에서는 PL/SQL, SQL Server에서는 T-SQL이라 부르고 MySQL에서는 그냥 SQL이라 일컫습니다.
위 그림과 같이 각 회사의 제품은 모두 표준 SQL을 공통으로 사용하면서 자기 제품의 특성에 맞춘 호환되지 않는 SQL 문도 사용합니다.
-
💉[SQL] DBMS의 분류.
💉[SQL] DBMS의 분류.
DBMS의 유형은 계층형(Hierarchical), 망형(Network), 관계형(Relational), 객체지향형(Object-Oriented), 객체관계형(Object-Relational) 등으로 분류됩니다.
현재 사용되는 DBMS 중에는 관계형 DBMS(Relational DBMS) 가 가장 많은 부분을 차지하며, MySQL 도 관계형 DBMS(Relational DBMS)에 포함됩니다.
1️⃣ 계층형 DBMS.
계층형 DBMS(Hierarchical DBMS) 는 처음으로 등장한 DBMS 개념으로 1960년대에 시작되었습니다.
다음 그림과 같이 각 계층은 트리(tree) 형태를 갖습니다.
계층형 DBMS의 문제는 처음 구성을 완료한 후에 이를 변경하기가 상당히 까다롭다는 것입니다.
또한, 다른 구성원을 찾아가는 것이 비효율적입니다.
예를 들어 재무 2팀에서 회계팀으로 연결하려면 재무이사 -> 사장 -> 회계이사 -> 회계팀과 같이 여러 단계를 거쳐야 합니다.
지금은 사용하지 않는 형태입니다.
2️⃣ 망형 DBMS.
망형 DBMS(Network DBMS) 는 계층형 DBMS의 문제점을 개선하기 위해 1970년대에 등장했습니다.
다음 그림을 보면 하위에 있는 구성원끼리도 연결된 유연한 구조입니다.
예를 들어 재무 2팀에서 바로 회계팀으로 연결이 가능합니다.
하지만 망형 DBMS를 잘 활용하려면 프로그래머가 모든 구조를 이해해야만 프로그램 작성이 가능하다는 단점이 존재합니다.
지금은 거의 사용하지 않는 형태입니다.
3️⃣ 관계형 DBMS
관계형 DBMS(Relational DBMS) 는 줄여서 RDBMS 라고 부릅니다.
MySQL 뿐만 아니라, 대부분의 DBMS가 RDBMS 형태로 사용됩니다.
RDBMS의 데이터베이스는 테이블(table) 이라는 최소 단위로 구성되며, 이 테이블은 하나 이상의 열(Column) 과 행(Row) 으로 이루어져있습니다.
아래의 표 모양이 바로 테이블입니다.
친구의 카카오톡 아이디, 이름, 연락처 등 3가지 정보를 표, 즉 테이블로 만들면 다음과 같습니다.
RDBMS 에서는 모든 데이터가 테이블에 저장됩니다.
이 구조가 가장 기본적이고 중요한 구성이기 때문에 테이블만 제대로 파악하면 RDBMS를 어느 정도 이해했다고 알 수 있습니다.
테이블은 열과 행으로 이루어진 2차원 구조를 갖습니다.
세로는 열(Column)이라 하고, 가로는 행(Row)이라고 합니다.
열은 아이디, 이름, 연락처로 이름을 가지고 있고, 행은 각각의 정보로 이루어져 있습니다.
-
🌐[Network] OSI 7계층 모델 - OSI 7계층 모델
🌐[Network] OSI 7계층 모델 - OSI 7계층 모델.
네트워크에 연결된 컴퓨터들이 데이터를 주고받으려면 서로 연동할 수 있게 표준화된 인터페이스를 지원해야 합니다.
일반적으로 컴퓨터 네트워크에서는 계층 구조로 모듈화된 프로토콜 스택(Protocol Stack)을 사용합니다.
국제 표준화 기구인 ISO가 확립한 OSI(Open System Interconnection) 7계층 모델은 개방화된 Open 데이터 통신 환경에 적합한 계층적 구현 모델의 표준입니다.
1️⃣ OSI 7계층 모델.
위 그림은 ISO(International Standard Organization)에서 제시한 OSI 7계층 모델(OSI 7 Layer Model)입니다.
연결된 두 호스트가 각각 7개 계층으로 구성된 모듈을 수행함으로써 데이터 송수신이 가능합니다.
전송 데이터는 송신 호스트의 응용 계층에서 시작해 하위 계층으로 순차적으로 전달되어, 최종적으로 물리 계층에서 수신 호스트에 전달됩니다.
수신 호스트에서는 데이터를 상위 계층으로 순차적으로 이동시켜 응용 계층까지 보내줍니다.
데이터가 하위 계층으로 내려갈 때는 각 계층의 프로토콜에서 정의한 헤더 정보가 추가됩니다.
물리 계층을 제외한 모든 계층에서 헤더 정보가 추가되고, 물리 계층은 단순히 데이터 링크 계층에서 수신한 데이터를 수신 호스트의 물리 계층에 전달합니다.
데이터를 수신하는 호스트에서는 반대로 상위 계층으로 올라가며 순차적으로 헤더 정보를 제거하고 해석하면서 프로토콜 기능을 수행합니다.
위 그림에 표시하지는 않았지만, 송신 호스트와 수신 호스트 사이에는 다수의 라우터 등이 존재하여 중개 기능을 합니다.
용어 정의.
임의의 호스트에서 실행되는 계층 n 모듈은 상태 호스트의 n 모듈과 논리적으로 통신하는데, 이들이 사용하는 규칙을 계층 n 프로토콜(Protocol)이라고 합니다.
프로토콜의 역할은 프로토콜에서 정의된 기능을 수행하면서 필요한 정보를 서로 교환하는 것입니다.
프로토콜 기능을 원활하게 수행하려면 주소 표현 방법, 오류 제어, 흐름 제어 등의 기능이 설계와 구현 과정에서 반영되어야 합니다.
동일 계층에 위치한 통신 양단은 같은 프로토콜을 사용하여 통신하기 때문에 동료 프로세스(Peer Process)라 합니다.
한 호스트에서 상하로 이웃하는 계층에 위치한 모듈 사이에는 인터페이스(Interface)가 정의되어 둘 사이의 접근 방법을 제한합니다.
상위 계층에서는 하위 계층의 인터페이스를 통해 하위 계층의 서비스(Service)를 이용할 수 있습니다.
송신 호스트에서 데이터를 전달할 때는 동료 프로세스에 직접 전달하는 것이 아니라, 하위 계층을 통하여 간접적으로 서비스를 요청합니다.
이 요청은 최하위에 있는 물리 계층까지 반복됩니다.
수신 호스트에서는 반대로 상위 계층으로 데이터가 전달되면서 프로토콜 기능이 동작합니다.
각 계층의 동료 프로세스가 직접 통신하는 형태를 보이지만, 실제로는 항상 물리 계층을 통해 데이터가 전송되는 것입니다.
헤더 정보.
프로토콜 스택의 맨 위에 위치한 일반 사용자는 전송 데이터가 있으면 이를 응용 계층에 보내 전송을 요청합니다.
응용 계층에서는 데이터에 자신의 프로토콜에서 정의한 헤더 정보를 추가해 표현 계층에 보냅니다.
표현 계층도 표현 계층에서 사용하는 프로토콜의 헤더 정보를 추가해 하위 계층으로 보냅니다.
이러한 일련의 과정은 물리 계층에서 데이터가 물리적으로 전송될 때까지 반복됩니다.
물리 계층에서는 수신 호스트에 데이터를 물리적으로 전송하고, 이를 수신한 호스트에서는 송신 절차와 반대 방향으로 헤더를 제거하는 과정이 반복됩니다.
즉, 계층별로 해당 계층의 헤더 정보를 해석하여 적절히 처리한 후에 상위 계층으로 올려줍니다.
따라서 송신 호스트에서 계층별로 추가된 헤더 정보가 수신 호스트에서 해석 및 삭제되어 최상위 수신사는 원래의 전송 데이터만 받습니다.
이때 각 계층의 프로토콜은 정해진 기능을 수행하여 데이터 송신 과정에서 발생하는 문제점을 해결해줍니다.
헤더 정보는 프로토콜마다 다르게 정의되며, 앞에서 설명한 주소, 오류 제어, 흐름 제어를 위한 정보들을 포함합니다.
프로토콜을 이해한다는 말은 프로토콜의 헤더 정보를 이해한다는 의미로 읽을 수 있을 정도로 헤더는 중요한 정보를 담고 있습니다.
특히, 인터넷의 기본 프로토콜인 TCP, UDP, IP의 헤더는 인터넷을 이해하는 데 많은 도움을 줍니다.
중개 기능.
위 그림에서 헤더 설명을 제외하고 중개 기능 설명을 추가하면 아래 그림이 됩니다.
그림과 같이 송신 호스트에서 수신 호스트로 데이터를 전달하려면 중개 역할을 수행하는 중개 노드를 거쳐야합니다.
중개 시스템은 데이터가 목적지까지 올바르게 전달되도록 경로 배정 기능을 수행합니다.
중개 시스템에서는 경로 배정 기능을 수행하는 네트워크 계층의 프로토콜이 동작하는데, 이와 같은 배정 기능을 라우팅(Routing)이라 합니다.
중개 노드인 라우터(Router)는 자신에게 도착한 데이터의 헤더 정보를 해석해서 적절한 경로를 선택해야 하며, 다음 라우터로 보내기 전에 헤더 정보를 수정하는 작업도 진행합니다.
라우터 좌우에 위치한 네트워크는 종류가 다를 수도 있습니다.
네트워크가 다르면 물리적인 특성뿐 아니라, 하위 계층의 헤더 정보도 다를 수 있습니다.
따라서 헤더 정보의 값을 해석하여 변환하는 작업은 라우터의 주요 기능에 해당합니다.
-
☕️[Java] 래퍼 클래스 - 자바 래퍼 클래스
☕️[Java] 래퍼 클래스 - 자바 래퍼 클래스.
1️⃣ 자바 래퍼 클래스.
래퍼 클래스는 기본형을 객체로 감싸서 더 편리하게 사용하도록 도와주기 때문에 상당히 유용하다.
즉, 래퍼 클래스는 기본형의 객체 버전입니다.
자바의 래퍼 클래스(Wrapper Class)는 기본 데이터 타입(primitive type)을 객체(object)로 다루기 위해 제공되는 클래스입니다.
자바에는 8개의 기본 데이터 타입이 있으며, 각각에 대응하는 래퍼 클래스가 있습니다.
래퍼 클래스는 기본 타입을 객체로 다뤄야 할 때 유용하게 사용됩니다.
기본형에 대응하는 래퍼 클래스.
byte -> Byte
short -> Short
int -> Integer
long -> Long
float -> Float
double -> Double
char -> Character
boolean -> Boolean
기본 재퍼 클래스의 특징.
불변이다.
equals로 비교해야 합니다.
자바가 제공하는 래퍼 클래스의 사용법.
package langReview.wrapper;
public class WrapperClassMain {
public static void main(String[] args) {
Integer newInteger = new Integer(10); // 미래에 삭제 예정, 대신에 valueOf()를 사용
Integer integerObj = Integer.valueOf(10); // -128 ~ 127 자주 사용하는 숫자 값 재사용, 불변
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
System.out.println("newInteger = " + newInteger);
System.out.println("integerObj = " + integerObj);
System.out.println("longObj = " + longObj);
System.out.println("doubleObj = " + doubleObj);
System.out.println("===================내부 값 읽기===================");
int intValue = integerObj.intValue();
System.out.println("intValue = " + intValue);
long longValue = longObj.longValue();
System.out.println("longValue = " + longValue);
System.out.println("===================비교===================");
System.out.println("== : " + (newInteger == integerObj));
System.out.println("equals : " + (newInteger.equals(integerObj)));
}
}
실행 결과
newInteger = 10
integerObj = 10
longObj = 100
doubleObj = 10.5
===================내부 값 읽기===================
intValue = 10
longValue = 100
===================비교===================
== : false
equals : true
1️⃣ 래퍼 클래스 생성 - 박싱(Boxing)
기본형을 래퍼 클래스로 변경하는 것을 마치 박스에 물건을 넣은 것 같다고 해서 박싱(Boxing) 이라고 합니다.
new Integer(10)은 직접 사용하면 안됩니다.
작동은 하지만, 향후 자바에서 제거될 예정입니다.
대신에 Integer.valueOf(10)을 사용하면 됩니다.
내부에서 new Integer(10)을 사용해서 객체를 생성하고 돌려줍니다.
추가로 Integer.valueOf()에는 성능 최적화 기능이 있습니다.
개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위의 Integer 클래스를 미리 생성해줍니다.
해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환합니다.
해당 범위의 값이 없으면 new Integer()를 호출합니다.
마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용합니다.
참고로 이런 최적화 방식은 미래에 더 나은 방식으로 변경될 수 있습니다.
2️⃣ intValue() - 언박싱(Unboxing)
래퍼 클래스는 객체이기 때문에 == 비교를 하면 인스턴스의 참조값(Reference)을 비교합니다.
래퍼 클래스는 내부의 값을 비교하도록 equals()를 재정의 해두었습니다.
따라서 값을 비교하려면 equals()를 사용해야 합니다.
참고로 래퍼 클래스는 객체를 그대로 출력해도 내부에 있는 값을 문자로 출력하도록 toString()을 재정의 해두었습니다.
2️⃣ 래퍼 클래스의 주요 사용 목적.
컬렉션과의 호환성.
자바의 컬렉션 프레임워크(List, Set, Map 등)는 객체만 저장할 수 있습니다.
따라서 기본 데이터 타입(primitive type)을 컬렉션에 저장하려면 해당 값을 래퍼 클래스의 객체로 변환해야 합니다.
유틸리티 메서드 제공.
래퍼 클래스는 기본 데이터 타입을 문자열로 변환하거나, 문자열을 기본 데이터 타입(primitive type)으로 변환하는 등의 유용한 유틸리티 메서드를 제공합니다.
상수 정의.
래퍼 클래스는 MAX_VALUE. MIN_VALUE 등과 같은 상수를 제공합니다.
이 상수들은 해당 타입의 최대값, 최소값 등을 표현합니다.
3️⃣ 래퍼 클래스의 주요 메서드.
valueOf(String s) : 문자열을 기본 데이터 타입의 래퍼 객체로 변환합니다.
parseInt(String s) : 문자열을 int로 변환합니다.
toString() : 객체의 값을 문자열로 변환합니다.
compareTo(T another) : 두 래퍼 객체를 비교하여 정수 값을 반환합니다.
4️⃣ 예시.
public class WrapperClassExample {
public static void main(String[] args) {
// 기본 타입을 래퍼 클래스로 변환 (박싱, Boxing)
Integer intObject = Integer.valueOf(42);
Double doubleObject = Double.valueOf(3.14);
Boolean booleanObject = Boolean.valueOf(true);
// 래퍼 클래스를 기본 타입으로 변환(언박싱, Unboxing)
int intValue = intObject.intValue();
double doubleValue = doubleObject.doubleValue();
boolean = booleanValue = booleanObject.booleanValue();
System.out.println("Integer object: " + intObject);
System.out.println("Double object: " + doubleObject);
System.out.println("Boolean object: " + booleanObject);
System.out.println("Unboxed int: " + intValue);
System.out.println("Unboxed double: " + doubleValue);
System.out.println("Unboxed boolean: " + booleanValue);
}
}
이 코드는 기본 데이터 타입과 래퍼 클래스 간의 변환을 보여주며, 박싱과 언박싱이 어떻게 동작하는지를 설명합니다.
이러한 기능들은 자바에서 기본 타입을 객체로 다루어야 하는 상황에서 매우 유용하게 사용됩니다.
-
-
💾 [CS] 이터레이터 패턴(Iterator pattern)
💾 [CS] 이터레이터 패턴(Iterator pattern).
1️⃣ 이터레이터 패턴(Iterator pattern).
이터레이터 패턴(Iterator pattern)은 이터레이터(Iterator)를 사용하여 컬렉션(collection)의 요소들에 접근하는 디자인 패턴입니다.
이를 통해 순회할 수 있는 여러 가지 자료형의 구조와는 상관없이 이터레이터라는 하나의 인터페이스로 순회가 가능합니다.
2️⃣ 이터레이터(Iterator)
이터레이터(Iterator)는 프로그래밍에서 컬렉션(예: 베열, 리스트, 셋 등) 내의 요소들을 순차적으로 접근할 수 있게 해주는 객체를 말합니다.
이터레이터는 주로 루프를 통해 컬렉션의 요소들을 하나씩 가져와 처리할 때 사용됩니다.
이터레이터의 핵심 기능 두 가지.
1. next() : 이터레이터의 다음 요소를 반환합니다. 다음 요소가 없을 경우 예외를 발생시키거나 특정 값을 반환할 수 있습니다.
2. hasNext() : 다음에 가져올 요소가 있는지 여부를 확인합니다. 다음 요소가 있으면 true를, 없으면 false를 반환합니다.
예시.
자바에서의 이터레이터 사용 예시는 다음과 같습니다.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}
위의 코드에서 list.iterator()를 통해서 리스트의 이터레이터를 얻고, while 루프를 통해 hasNext()로 다음 요소가 있는지 확인하면서 next()를 사용해 각 요소를 하나씩 출력합니다.
이터레이터는 컬렉션의 요소를 순차적으로 탐색할 수 있는 표준화된 방법을 제공하기 때문에, 컬렉션이 무엇이든 상관 없이 동일한 방식으로 접근할 수 있습니다.
또한, 이터레이터를 사용하면 컬렉션 내부 구현에 직접 접근하지 않고도 요소들을 탐색할 수 있기 때문에 컬렉션의 안전한 접근과 수정이 가능합니다.
3️⃣ 자바에서의 이터레이터 패턴.
자바에서 이터레이터 패턴(Iterator Pattern)은 컬렉션 내부 구조를 노출하지 않고도 그 요소들에 순차적으로 접근할 수 있도록 하는 디자인 패턴입니다.
이 패턴은 java.util.Iterator 인터페이스를 통해 자바의 표준 라이브러리에서 널리 사용됩니다.
1. 기본 개념.
이터레이터 패턴은 컬렉션의 내부 구조를 숨기면서 요소들에 접근할 수 있게 해줍니다.
이 패턴은 반복자가 컬렉션 요소들을 순차적으로 탐색할 수 있는 메서드들을 정의합니다.
2. Iterator 인터페이스.
자바의 Iterator 인터페이스는 세 가지 주요 메서드를 가지고 있습니다.
boolean hasNext() : 다음에 읽어올 요소가 있는지 확인합니다. 있으면 true, 없으면 false를 반환합니다.
E next() : 다음 요소를 반환하고, 이터레이터를 다음 위치로 이동시킵니다.
void remove() : 이터레이터가 마지막으로 반환한 요소를 컬렉션에서 제거합니다.(이 메서드는 선택적으로 구현될 수 있습니다.)
3. 사용 예시.
먼저, 컬렉션 클래스(예: ArrayList, HashSet 등)의 이터레이터를 사용하여 요소들을 반복 처리하는 예시를 보겠습니다.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorPatternExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
}
}
4. 커스텀 이터레이터 구현.
자신만의 컬렉션 클래스와 그에 대한 이터레이터를 직접 구현할 수도 있습니다.
예를 들어, 간단한 Book 컬렉션과 그 이터레이터를 구현할 수 있습니다.
import java.util.Iterator;
import java.util.NoSuchElementException;
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
class BookCollection implements Iterable<Book> {
private Book[] books;
private int index = 0;
public BookCollection(int size) {
books = new Book[size];
}
public void addBook(Book book) {
if (index < books.length) {
books[index++] = books;
}
}
@Override
public Iterator<Book> iterator() {
return new BookIterator();
}
private class BookIterator implements Iterator<Book> {
private int currentIndex = 0;
@Override
public boolean hasNext() {
return currentIndex < book.length && books[currentIndex] != null;
}
@Override
public Book next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return books[currentIntex++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
}
}
public class Main {
public static void main(String[] args) {
BookCollection bookCollection = new BookCollection(3);
bookCollection.addBook(new Book("The Catcher in th Rye"));
bookCollection.addBook(new Book("To Kill a Mockingbird"));
bookCollection.addBook(new Book("1984"));
for (Book book : bookCollection) {
System.out.println(book.getTitle());
}
}
}
5. 동작 원리.
BookCollection 클래스는 Iterable<Book>을 구현하여 이터레이터 패턴을 따릅니다.
iterator() 메서드는 BookIterator 라는 내부 클래스를 반환하며, 이 클래스는 Iterator<Book>을 구현합니다.
BookIterator는 hasNext(), next(), 그리고 remove() 메서드를 구현하여 컬렉션의 요소들을 반복 처리합니다.
6. 마무리.
이터레이터 패턴은 이처럼 내부 구조를 감추고, 표준화된 방법으로 컬렉션의 요소들을 탐색할 수 있게 해줍니다.
이는 코드의 재사용성과 유지 보수성을 높이는 데 기여합니다.
-
💉[SQL] DBMS의 발전 과정.
💉[SQL] DBMS의 발전 과정.
컴퓨터가 존재하기 전부터 사람들은 데이터(정보)를 관리해 왔습니다.
종이에 정보를 기록하고 관리하던 때부터 시작해 지금의 DBMS까지 어떤 과정으로 발전했는지 차례대로 살펴봅시다.
1️⃣ 종이에 펜으로 기록.
아주 오래 전부터 정보는 관리되어 왔습니다.
컴퓨터가 없던 시기에도 구멍가게(요즘의 편의점과 비슷)를 운영하면서 판매와 구매가 발생했을 것이고, 그것을 종이에 펜으로 기록했을 것입니다.
2️⃣ 컴퓨터에 파일로 저장.
컴퓨터가 등장하고 일반 사람들도 컴퓨터를 사용하게 되면서 종이에 기록하던 내용을 컴퓨터 파일에 기록, 저장하게 되었습니다.
컴퓨터에 판매/구매 이력을 저장하는 방법은 단순하게 메모장을 사용할 수도 있지만, 컴퓨터를 어느 정도 활용하게 되면서 엑셀과 같은 스프레드시트 프로그램을 사용해 표 형태로 내용을 기록하고 자동으로 계산하는 등 한층 더 효율적으로 정보를 관리하게 되었습니다.
기록된 내용은 파일(file) 이라는 형태로 저장해 필요할 때마다 열어서 사용할 수 있습니다.
엑셀을 사용하면 아주 편리하지만, 저장한 파일은 한 번에 한 명의 사용자만 열어서 작업할 수 있습니다.
규모가 작은 구멍가게에서는 한 명의 사용자가 하나의 파일에 작업하는 것이 문제가 되지 않을 수도 있습니다.
하지만 규모가 큰 슈퍼마켓이나 마트 등에서는 데이터의 양이 많아 한 명의 사용자가 모두 처리할 수 없기 때문에 여러 명이 각자의 파일을 만들어서 작업할 수밖에 없습니다.
예시.
예를 들어, 3명의 직원이 엑셀로 판매 내용을 기록한다고 합시다.
A 직원은 오전, B 직원은 오후, C 직원은 야간에 판매된 내용을 기록한다고 가정하겠습니다.
3명이 정확히 자신의 시간에 판매된 것만 기록하면 좋겠으나, 실수로 A 직원이 판매한 내역을 B 직원 파일에 작성할 수도 있을 것입니다.
또, 오전에 판매한 물건을 오후에 반품할 경우에는 오전에 판매한 사람이 기록해야 할지, 오후에 반품받은 사람이 기록해야 할지 그 주체도 모호하기 때문에 기록이 누락되거나 모두 기록하여 중복되는 문제가 발생할 소지도 있습니다.
하루, 한 달 더 나아가서는 연간 판매 기록을 합계할 때 금액이 맞지 않는 경우처럼 심각한 일이 발생할 수도 있습니다.
이러한 불일치가 파일의 큰 문제점 중 하나입니다.
하지만 이런 문제점에도 불구하고 파일은 한 명이 처리하거나 소량의 데이터를 처리할 때는 속도가 빠르고, 사용법이 쉽기 때문에 지금도 많이 사용하고 있습니다.
3️⃣ DBMS의 대두와 보급.
앞에서 언급한 파일의 단점을 보완하면서 대량의 데이터를 효율적으로 관리하고 운영하기 위해서 등장한 것이 DBMS 입니다.
MySQL과 같은 DBMS의 개념은 1973년에 최초로 에드거 프랭크 커드(E.F. Codd)라는 학자가 이론을 정립했습니다.
그 이후로 많은 DBMS 제품이 만들어졌고, 지금과 같이 안정적인 소프트웨어로 자리 잡게 되었습니다.
DBMS는 데이터의 집합인 데이터베이스 를 잘 관리하고 운영하기 위한 시스템 또는 소프트웨어를 말합니다.
DBMS에 데이터를 구축, 관리하고 활용하기 위해서 사용되는 언어가 SQL(Structured Query Language) 입니다.
이 SQL을 사용하면 DBMS를 통해 중요한 정보들을 입력, 관리하고 추출할 수 있습니다.
즉, SQL 문을 잘 이해하고 사용해야만 DBMS를 원활하게 활용할 수 있습니다.
비유하자면 미국 문화(DBMS)를 완전히 이해하고 싶다면 그 나라의 언어인 영어(SQL)를 먼저 배워야 하는 것과 비슷한 개념입니다.
-
🌐[Network] 네트워크 개념 - 서비스 프리미티브
🌐[Network] 네트워크 개념 - 서비스 프리미티브.
1️⃣ 서비스 프리미티브.
프로토콜은 계층 구조로 이루어져 있고, 하위 계층이 상위 계층에 서비스를 제공하는 방식으로 동작합니다.
이러한 서비스는 다음에 살펴볼 프리미티브(Primitive) 형태로 구현됩니다.
계층 구조 프로토콜에서 하위 계층이 상위 계층에 제공하는 서비스의 종류에는 연결형과 비연결형이 있습니다.
연결형 서비스.
연결형(Connection-oriented) 서비스를 이용하는 절차는 크게 3단계 입니다.
먼저 데이터 전달 경로를 설정하는 연결 설정 단계가 필요합니다.
이 단계가 성공적으로 수행되어 연결이 설정되어야 다음 단계인 데이터 전송이 가능합니다.
모든 데이터의 전송이 완료되어 데이터 전송 단계를 끝내려면 연결을 끊는 연결 해제 단계가 필요합니다.
연결형 서비스의 동작 원리는 전화 시스템을 이용한 통화 절차와 매우 유사합니다.
비연결형 서비스.
비연결형(Connectionless) 서비스는 우편 시스템의 동작 원리와 비슷합니다.
연결을 설정하고 해제하는 단계가 필요 없습니다.
즉, 전송할 데이터가 있으면 각 데이터를 독립적으로 목적지 호스트로 전송하면 됩니다.
데이터는 독립적인 경로 선택 과정에 의해 전달되므로 도착하는 순서가 보낸 순서와 일치하지 않을 수 있습니다.
하위 계층이 상위 계층에 제공하는 서비스는 프리미티브(Primitive) 형태로 구현됩니다.
따라서 프리미티브는 하위 계층을 사용하는 방법을 정형화한 것입니다.
연결형 서비스에서 추상화될 수 있는 기본 서비스 프리미티브의 종류에는 아래의 표처럼 CONNECT, DATA, DISCONNECT가 있습니다.
프리미티브
용도
CONNECT
연결 설정
DATA
데이터 전송
DISCONNECT
연결 해제
통신 프로토콜에서 프리미티브를 올바르게 수행하려면 각 프리미티브가 아래의 표의 네 가지 기능을 포함하도록 설계해야 합니다.
표에서는 설명의 편리함을 위하여 클라이언트와 서버라는 용어를 사용했으며, 아래 그림에서는 왼쪽 호스트를 클라이언트, 오른쪽 호스트를 서버로 가정했습니다.
클라이언트에서 서버로 전달되는 요청은 Request와 Indication으로 구현되고, 서버의 응답은 Response와 Confirm으로 구현됩니다.
기능
설명
Request
클라이언트가 서버에 서비스를 요청함
Indication
서버에 서비스 요청이 도착했음을 통지함
Response
서버가 클라이언트에 서비스 응답을 회신함
Confirm
클라이언트에 응답이 도착했음을 통지함
아래 그림은 클라이언트와 서버 사이에서 서비스 프리미티브가 처리되는 원리를 설명하고 있습니다.
클라이언트의 상위 계층이 하위 계층에 Request를 요청하면 이 요청은 하위 계층 아래에 있는 차하위 계층의 도움을 받아 서버의 하위 계층에 전달됩니다.
그러면 이 요청은 서버의 상위 계층에 Indication 형태로 전달되어 서버가 클라이언트 요청을 인지합니다.
서버에서는 해당 프리미티브를 올바르게 수신하였음을 클라이언트에 통보하기 위하여 Response를 응답으로 보내고, 반대의 과정을 거쳐서 클라이언트에 Confirm 형태로 도착합니다.
이와 같은 4단계 절차를 통해 하나의 서비스 프리미티브가 처리됩니다.
위의 그림의 4단계 절차를 전화 시스템의 연결 설정인 CONNECT에 적용하여 설명하면 다음과 같습니다.
전화 거는 사람을 클라이언트, 전화 받는 사람을 서버로 가정하면, 통신 회사의 전화망은 클라이언트와 서버 기능을 모두 수행하는 하위 계층이 됩니다.
발신자가 전화번호를 누르면(Request) 전화망은 이 전화번호에 해당하는 전화기의 위치를 찾아 수신자의 전화벨(Indication)이 울리게 합니다.
수신자가 전화를 받기 위해 통화 버튼을 누르면(Request) 전화망은 이 사실을 바로 인지할 수 있습니다.
수신자가 통화 버튼을 누름과 동시에 발신자 전화기의 발신음이 끊기면서(Confirm) 통화 연결 상태가 되었음을 확인할 수 있습니다.
2️⃣ 네 가지 서비스 프리미티비의 기능.
네 가지 서비스 프리미티브의 기능을 요약하여 설명하면 다음과 같습니다.
여기서 상위 계층과 사위 계층을 프로토콜이라는 용어로 설명합니다.
Request
Request는 클라이언트가 서버에 프리미티브의 기능을 수행하도록 요청하는 것입니다.
클라이언트에서는 상위 프로토콜이 하위 프로토콜에 요청을 전달하고, 이 요청이 가장 아래의 물리 계층을 통하여 서버에 전달됩니다.
연결 설정 요청(CONNECT.Request), 데이터 전송 요청(DATA.Request), 연결 해제 요청(DISCONNECT.Request) 등이 있습니다.
Indication
물리 계층을 통하여 Request 요청을 수신한 서버는 이 요청을 물리 계층 위에 있는 하위 프로토콜에 전달합니다.
이후, Indication을 사용해서 상위 프로토콜에 프리미티브 요청이 발생했음을 알립니다.
연결 설정, 데이터 전송, 연결 해제에 대해 CONNECT.Indication, DATA.Indication, DISCONNECT.Indication 순으로 사용합니다.
Response
클라이언트로부터 프리미티브를 받은 서버에서는 Response를 이용해 클라이언트에 응답합니다.
응답의 전달 과정은 Request, indication과 반대의 순서로 진행되며, 연결 설정 요청에 대해서는 CONNECT.Reponse를 사용해 연결 허용이나 거부로 응답하고, 데이터는 DATA.Response, 연결 해제는 DISCONNECT.Response로 전달합니다.
Confirm
서버에서 보낸 응답은 Confirm 형태로 클라이언트에 회신됩니다.
연결 설정은 CONNECT.Confirm, 데이터는 DATA.Confirm, 연결 해제는 DISCONNECT.Confirm로 전달됩니다.
-
💾 [CS] CORS란?
💾 [CS] CORS란?
1️⃣ CORS.
CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 서로 다른 출처(도메인, 프로토콜, 또는 포트)를 가진 리소스 간의 요청을 안전하게 수행할 수 있도록 하는 보안 기능입니다.
CORS(Cross-Origin Resource Sharing)는 웹 페이지가 다른 출처의 리소스를 요청할 때 발생하는 보안 제약을 해결하기 위해 설계되었습니다.
2️⃣ 왜 CORS가 필요한가?
웹 보안 모델에서 동일 출처 정책(Same-Origin Policy)은 보안상의 이유로, 웹 페이지에서 로드된 자바스크립트가 자신이 로드된 출처 외부의 리소스에 접근하는 것을 제한합니다.
즉, 한 웹 페이지에서 로드된 스크립트는 다른 출처의 리소스(예: API 엔드포인트, 이미지 등)에 접근할 수 없습니다.
이 보안 정책은 사이트 간 요청 위조(CSRF)와 같은 공격을 방지하는 데 중요한 역할을 합니다.
하지만, 많은 경우 애플리케이션은 외부 API나 다른 도메인에 있는 리소스에 접근해야 할 필요가 있습니다.
이때, CORS를 사용하여 특정 출처에서 오는 요청을 허용할 수 있습니다.
3️⃣ CORS의 동작 방식.
CORS는 서버가 클라이언트의 요청에 대해 다른 출처에서의 접근을 허용할지 여부를 HTTP 헤더를 통해 명시합니다.
CORS를 구현하는 과정.
1. Preflight Request
브라우저는 실제 요청을 보내기 전에 OPTIONS 메서드를 사용해 서버에 “사전 요청(preflight request)”을 보냅니다.
이 요청은 클라이언트가 보내려고 하는 실제 요청의 메서드와 헤더가 서버에서 허용되는지 확인합니다.
2. 서버 응답
서버는 Access-Control-Allow-Origin 등의 CORS 관련 헤더를 포함한 응답을 반환합니다.
이 응답을 통해 브라우저는 해당 출처의 요청을 허용할지 결정합니다.
3. 실제 요청
서버가 허용한 경우, 브라우저는 실제 요청을 보내고 서버에서 데이터를 받아옵니다.
4️⃣ 주요 CORS 헤더.
Access-Control-Allow-Origin
클라이언트가 접근을 허용받은 출처를 지정합니다.
모든 출처를 허용하려면 *를 사용할 수 있습니다.
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods
서버가 허용하는 HTTP 메서드(GET, POST, PUT, DELETE 등)를 지정합니다.
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials
서버가 클라이언트의 자격 증명(쿠키, 인증 헤더 등)을 포함한 요청을 허용할지 여부를 결정합니다.
값으로는 true 또는 false가 올 수 있습니다.
Access-Control-Allow-Credentials: true
Access-Control-Max-Age
브라우저가 사전 요청의 결과를 캐시할 수 있는 시간을 초 단위로 지정합니다.
Access-Control-Max-Age: 3600
5️⃣ CROS 설정 예시(서버 측)
서버 측에서 CORS를 설정하는 방법은 사용 중인 웹 서버 또는 프레임워크에 따라 다릅니다.
다음은 Java SpringBoot 애플리케이션에서 CORS를 설정하는 예입니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowedCredentials(true)
.maxAge(3600);
}
}
위 설정은 특정 출처(https://example.com)에서 오는 모든 경로의 요청에 대해 CORS를 허용하고, 특정 HTTP 메서드와 헤더를 사용할 수 있도록 합니다.
6️⃣ 요약.
CORS 는 웹 브라우저가 서로 다른 출처의 리소스에 안전하게 접근할 수 있도록 하기 위한 보안 기능입니다.
CORS 는 서버가 응답 헤더를 통해 특정 출처의 요청을 허용할 수 있도록 하며, 이를 통해 웹 애플리케이션은 외부 API나 리소스에 접근할 수 있습니다.
CORS 를 적절히 설정하면, 보안성을 유지하면서도 다양한 출처에서의 리소스 접근을 허용할 수 있습니다.
-
💾 [CS] CDN 서비스란?
💾 [CS] CDN 서비스란?
1️⃣ CDN 서비스.
CDN(Content Delivery Network) 서비스는 전 세계에 분산된 서버 네트워크를 통해 웹 콘텐츠를 사용자에게 신속하고 효율적으로 전달하는 서비스입니다.
CDN은 웹사이트의 성능을 향상시키고, 대규모 트래픽을 효율적으로 관리하며, 전 세계 사용자에게 일관된 사용자 경험을 제공하는 데 중요한 역할을 합니다.
2️⃣ CDN의 주요 기능.
1. 콘텐츠 캐싱(Caching).
CDN은 원본 서버(origin server)로부터 자주 요청되는 콘텐츠(예: 이미지, 동영상, JavaScript, CSS 파일 등)를 캐싱 서버에 저장합니다.
사용자가 콘텐츠를 요청하면, CDN은 지리적으로 가장 가까운 서버에서 캐싱된 콘텐츠를 제공하여 응답 시간을 줄입니다.
2. 분산된 네트워크.
CDN은 전 세계에 분산된 여러 서버(엣지 서버, Edge Servers)로 구성도어 있습니다.
사용자의 요청은 지리적으로 가장 가까운 엣지 서버로 라우팅되며, 이를 통해 데이터 전송 거리를 최소화하고 전송 속도를 향상시킵니다.
3. 로드 밸런싱.
CDN은 여러 서버 간에 트래픽을 분산시켜 서버에 과부하가 걸리는 것을 방지합니다.
이를 통해 안정적인 서비스 제공과 성능 저하를 방지합니다.
4. 보안 강화.
CDN은 DDoS(Distributed Denial of Service) 공격 방어, SSL/TLS 암호화, WAF(Web Application Firewall) 등 다양한 보안 기능을 제공하여 웹사이트를 보호합니다.
5. 고가용성 및 장애 복구
CDN은 서버가 장애를 겪을 경우에도 다른 서버에서 서비스를 제공할 수 있어, 웹사이트의 가용성을 높이고 장애 복구 능력을 향상시킵니다.
3️⃣ CDN 서비스의 장점.
1. 빠른 콘텐츠 제공.
CDN은 사용자와 가장 가까운 서버에서 콘텐츠를 제공하므로, 로딩 속도가 빨라지고 사용자 경험이 향상됩니다.
2. 트래픽 관리.
대규모 트래픽이 발생하는 웹사이트나 이벤트에서도 CDN은 트래픽을 효율적으로 분산시켜 서버 과부하를 방지하고 안정적인 서비스를 유지할 수 있습니다.
3. 전 세계적인 도달 범위.
CDN은 전 세계에 분산된 서버를 통해 다양한 지역의 사용자에게 일관된 성능을 제공합니다.
이는 특히 글로벌 서비스를 제공하는 웹사이트에 중요합니다.
4. 비용 절감.
CDN은 원본 서버로의 요청을 줄여 서버 부하를 줄이고, 데이터 전송 비용을 절감할 수 있습니다.
5. 보안 강화.
CDN은 다양한 보안 기능을 제공하여 웹사이트를 공격으로부터 보호할 수 있습니다.
4️⃣ CDN 서비스의 예시.
Akamai : 가장 오래된 CDN 제공업체 중 하나로, 다양한 콘텐츠 전송 및 보안 솔루션을 제공합니다.
ClouldFlare : CDN과 함께 DDoS 방어, WAF, DNS 관리 등의 기능을 제공하는 인기 있는 서비스입니다.
Amazon CloudFront : AWS의 CDN 서비스로, 다른 AWS 서비스와의 통합이 용이합니다.
Google Clould CDN : Google Cloud Platform에서 제공하는 CDN 서비스로, 글로벌 인프라를 활용해 빠르고 안정적인 콘텐츠 제공을 지원합니다.
Fastly : 실시간 콘텐츠 업데이트와 사용자 지정 가능성이 뛰어난 CDN 서비스입니다.
5️⃣ 요약.
CDN 서비스는 전 세계에 분산된 서버 네트워크를 통해 웹 콘텐츠를 효율적으로 전달하는 서비스로, 웹사이트의 성능을 향상시키고 보안을 강화하며, 대규모 트래픽을 처리하는 데 중요한 역할을 합니다.
이를 통해 사용자 경험을 개선하고, 글로벌 사용자에게 빠르고 안정적인 서비스를 제공할 수 있습니다.
-
☕️[Java] null이란?
☕️[Java] null이란?
1️⃣ null.
Java에서 null은 참조형 타입(Reference Type) 변수에 사용되는 특별한 값으로, 해당 변수가 어떤 객체도 가리키지 않고 있음을 나타냅니다.
즉, null은 변수에 객체의 참조가 없음을 의미합니다.
2️⃣ 주요 특징.
1. 참조형 타입에만 적용.
null은 기본형 타입(Primitive Type)에는 사용할 수 없으며, 오직 참조형 타입(예: String, Integer, List 등) 변수에만 사용할 수 있습니다.
기본형 타입의 변수는 null이 아닌, 각 타입의 기본값(예: int는 0, boolean은 false)을 가집니다.
2. 객체의 부재.
null은 변수에 객체가 할당되지 않았거나, 명시적으로 “객체가 없음”을 나타내고자 할 때 사용됩니다.
예를 들어, 객체가 생성되지 않거나 초기화되지 않은 상태에서 참조형 변수를 선언하면, 해당 변수는 기본적으로 null 값을 가집니다.
3. NullPointerException.
null 참조를 사용하려고 할 때(예: null 객체의 메서드를 호출하거나 필드에 접근할 때), Java는 NullPointerException을 발생시킵니다.
이는 매우 일반적인 런타임 오류 중 하나입니다.
3️⃣ null의 사용 예.
public class Main {
public static void main(String[] args) {
String str = null; // str은 어떤 객체도 가리키지 않음
if (str == null) {
System.out.println("str은 null입니다.");
}
// NullPointerException을 피하기 위해 null 검사를 수행해야 합니다.
if (str != null) {
System.out.println(str.length()); // str이 null이 아닌 경우에만 실행
}
// 만약 null 검사를 하지 않고 null 참조를 사용하면 NullPointerException 발생
try {
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("NullPointerException 발생: " + e.getMessage());
}
}
}
위 코드에서 str 변수는 null로 초기화되었습니다.
str.length()를 호출하려고 하면, NullPointerException이 발생합니다.
이 오류를 피라기 위해서는 항상 null 검사를 수행해야 합니다.
4️⃣ null 사용의 주의점.
1. NullPointerException 방지.
코드를 작성할 때, null일 가능성을 항상 염두에 두어야 합니다.
null 검사를 적절히 수행하지 않으면 NullPointerException이 발생할 수 있습니다.
2. Optional 사용.
Java 8 이후에는 Optional 클래스를 사용하여 null을 안전하게 처리할 수 있습니다.
Optional은 값이 있거나 없음을 명시적으로 나타내어 null 참조를 피할 수 있게 해줍니다.
```java
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional optionalStr = Optional.ofNullable(null);
if (optionalStr.isPresent()) {
System.out.println(optionalStr.get());
} else {
System.out.println("값이 존재하지 않습니다.");
}
} } ```
위 코드에서 Optional.ofNullable(null)을 사용하여 null을 안전하게 처리할 수 있습니다.
5️⃣ 요약.
null은 참조형 타입 변수가 어떤 객체도 참조하지 않고 있음을 나타냅니다.
null을 잘못 사용하면 NullPointerException이 발생할 수 있으며, 이를 방지하기 위해서는 null 검사를 철저히 수행해야 합니다.
Java 8 이후에는 Optional 클래스를 사용하여 null 참조를 보다 안전하게 관리할 수 있습니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계(2)
☕️[Java] 래퍼 클래스 - 기본형의 한계(2)
1️⃣ 기본형과 null.
기본형(Primitive Type)은 항상 값을 가져야 합니다.
하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다.
public class MyIntegerNullMain0 {
public static void main(String[] args) {
int[] intArr = {-1, 0, 1, 2, 3};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // -1
}
private static int findValue(int[] intArr, int target) {
for (int value : intArr) {
if (value == target) {
return value;
}
}
return -1;
}
}
findValue()는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환합니다.
findValue()는 결과로 int를 반환합니다.
int와 같은 기본형은 항상 값이 있어야 합니다.
여기서도 값을 반환할 때 값을 찾지 못하면 숫자 중에 하나를 반환해야 하는데 보통 -1 또는 0을 사용합니다.
실행 결과
-1
0
1
-1
실행 결과를 보면 입력값이 -1일 때 -1을 반환합니다.
그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환합니다.
입력값이 -1일 때를 생각해보면, 배열에 -1 값이 있어서 -1을 반환한 것인지, 아니면 -1 값이 없어서 -1을 반환한 것인지 명확하지 않습니다.
2️⃣ 참조형과 null.
객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다.
public class MyIntegerNullMain1 {
public static void main(String[] args) {
MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // null
}
private static MyInteger findValue(MyInteger[] intArr, int target) {
for (MyInteger myInteger : intArr) {
if (myInteger.getValue() == target) {
return myInteger;
}
}
return null;
}
}
실행 결과
-1
0
1
null
이전에 만든 MyInteger 래퍼 클래스를 사용했습니다.
실행 결과를 보면 -1을 입력했을 때는 -1을 반환합니다.
100을 입력했을 때는 값이 없다는 null을 반환합니다.
3️⃣ 요약.
기본형은 항상 값이 존재해야 합니다.
숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.
객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.
null 값을 반환하는 경우 잘못하면 NullPointException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계(1)
☕️[Java] 래퍼 클래스- 기본형의 한계(1)
1️⃣ 기본형의 한계.
자바는 객체 지향 언어입니다.
그런데 자바 안에 객체가 아닌 것이 있습니다.
바로 int, double 같은 기본형(Primitive Type)입니다.
기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다.
객체가 아님.
기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다.
예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서르듣 제공할 수 없습니다.
null 값을 가질 수 없습니다.
기본형 데이터 타입은 null 값을 가질 수 없습니다.
때로는 데이터가 없음이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다.
2️⃣ 예시.
기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해 보겠습니다.
왼쪽의 값이 더 작다: -1
두 값이 같다: 0
왼쪽의 값이 더 크다: 1
public class MyIntegerMethodMain0 {
public static void main(String[] args) {
int value = 10;
int i1 = compareTo(value, 5);
int i2 = compareTo(value, 10);
int i3 = compareTo(value, 20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
public static int compareTo(int value, int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
여기서는 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교합니다.
그런데 자기 자신인 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용됩니다.
이런 경우 만약 value가 객체라면 value 객체 스스로 자기 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다.
3️⃣ 직접 만든 래퍼 클래스.
int를 클래스로 만들어 봅시다.
int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다.
다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다.
이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다.
public class MyInteger {
private final int value;
public MyInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public int compareTo(int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
@Override
public String toString() {
return String.valueOf(value); // 숫자를 문자로 변경
}
}
MyInteger는 int value라는 단순한 기본형 변수 하나를 가지고 있습니다.
그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공합니다.
위에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했습니다.
이 클래스는 불변으로 설계했습니다.
MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다.
덕분에 데이터 조각에 불과한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다.
```java
public class MyIntegerMethodMain1 {
public static void main(String[] args) {
MyInteger myInteger = new MyInteger(10);
int i1 = myInteger.compareTo(5);
int i2 = myInteger.compareTo(10);
int i3 = myInteger.compareTo(20);
System.out.println(“i1 = “ + i1);
System.out.println(“i2 = “ + i2);
System.out.println(“i3 = “ + i3);
}
}
```
실행 결과
i1 = 1
i2 = 0
i3 = -1
myInteger.compareTo()는 자기 자신의 값을 외부의 값과 비교합니다.
MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있습니다.
참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
-
☕️[Java] Optional
☕️[Java] Optional.
1️⃣ Optional.
Optional은 Java 8에서 도입된 클래스이며, 주로 null 값을 안전하게 처리하기 위해 사용됩니다.
Optional은 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 표현하는 컨테이너 역할을 하며, null을 직접 사용하는 것보다 안전하고 명시적인 방법을 제공합니다.
2️⃣ 주요 특징
1. 값의 존재 여부를 명확히 표현.
Optional은 값이 존재하는 경우(Optional이 비어있지 않음)와 값이 존재하지 않는 경우(Optional이 비어있음)를 명확하게 표현합니다.
이를 통해 null 참조로 인해 발생할 수 있는 오류를 예방할 수 있습니다.
2. 명시적인 null 처리.
Optional을 사용하면 값이 없을 때 취할 행동을 명시적으로 정의할 수 있습니다.
이로 인해 코드가 더 읽기 쉬워지고, NullPointerException을 피할 수 있습니다.
3. 함수형 프로그래밍 스타일 지원.
Optional은 다양한 메서드(map, flatMap, filter, ifPresent, orElse 등)를 제공하여 값의 존재 여부에 따라 함수형 프로그래밍 스타일로 작업을 수행할 수 있습니다.
3️⃣ Optional 생성.
Optional<String> emptyOptional = Optional.empty();
빈 Optional 생성 : Optional.empty()를 사용하여 비어 있는 Optional을 생성할 수 있습니다.
Optional<String> nonEmptyOptional = Optional.of("Hello");
null이 아닌 값으로 Optional 생성.
Optional.of()를 사용하여 null이 아닌 값으로 Optional을 생성할 수 있습니다.
값이 null인 경우 NullPointerException이 발생합니다.
Optional<String> nullableOptional = Optional.ofNullable(null);
null이 될 수 있는 값으로 Optional 생성.
Optional.ofNullable()을 사용하여 값이 null일 수도 있는 경우 Optional을 생성할 수 있습니다.
값이 null이면 빈 Optional이 생성됩니다.
Optional<String> nullableOptional = Optional.ofNullable(null);
4️⃣ Optional의 사용 예
1. 값이 존재하는지 확인(isPresent / ifPresent)
```java
Optional optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println(optional.get()); // 값이 있으면 출력
}
optional.ifPresent(value -> System.out.println(value)); // 값이 있으면 람다 표현식 실행
- **2. 값이 없을 때 대체 값 제공(`orElse`, `orElseGet`)**
```java
String result = optional.orElse("Default Value"); // 값이 없으면 "Defaul Value"
String result = optional.orElseGet(() -> "Default Value from Supplier"); // 람다로 대체 값 제공
3. 값이 없으면 예외 던지기(orElseThrow)
String result = optional.orElseThrow(() -> new IllegalArgumentException("No value present"));
4. 값 변환(map, flatMap)
```java
Optional optional = Optional.of("Hello");
Optional lengthOptional = optional.map(String::length);
lengthOptional.ifPresent(length -> System.out.println("Length: " + length));
- **5. 조건에 따른 필터링(`filter`)**
```java
Optional<String> optional = Optional.of("Hello");
Optional<String> filtered = optional.filter(value.startsWith("H"));
filtered.ifPresent(System.out::println); // "Hello" 출력
5️⃣ Optional 사용의 이점.
1. 명시적 코드 작성.
Optional을 사용하면 코드에서 값이 존재할 수 있는지 여부를 명시적으로 다루게 되어 가독성이 높아집니다.
2. NullPointerException 방지.
Optional은 null 체크를 강제하여 NullPointerException 발생을 예방합니다.
3. 함수형 스타일.
Optional은 함수형 프로그래밍 스타일을 지원하여 간결하고 선언적인 코드를 작성할 수 있게 합니다.
6️⃣ Optional 사용 시 주의점.
1. 과도한 사용 자제.
모든 상황에서 Optional을 사용하는 것은 오히려 복잡성을 초래할 수 있습니다.
예를 들어, 필드에 Optional을 사용하거나, 매우 단순한 로직에 Optional을 사용하는 것은 바람직하지 않을 수 있습니다.
2. 성능.
Optional은 추가적인 객체를 생성하므로, 성능이 중요한 곳에서는 주의해서 사용해야 합니다.
7️⃣ 요약.
Optional은 null을 안전하게 처리하기 위한 클래스이며, 값이 있을 수도 있고 없을 수도 있는 상황을 명시적으로 표현할 수 있게 해줍니다.
Optional을 통해 코드의 가독성을 높이고, NullPointerException을 방지할 수 있습니다.
Optional은 함수형 프로그래밍 스타일을 지원하여 코드 작성의 유연성을 제공합니다.
-
☕️[Java] 람다(Lambda)
☕️[Java] 람다(Lambda).
1️⃣ 람다 표현식(Lambda Expression)
Java 8에서 도입된 기능으로, 익명 함수(Anonymous Function)를 간결하게 표현한 것입니다.
람다 표현식은 메서드로 전달되거나, 변수에 할당될 수 있는 짧은 형태의 코드를 작성할 수 있게 해줍니다.
주로 함수형 인터페이스(Functional Interface)를 구현하는 데 사용되며, Java에서 함수형 프로그래밍 스타일을 가능하게 합니다.
2️⃣ 람다 표현식의 기본 구조.
람다 표현식은 다음과 같은 형태로 작성됩니다.
(parameters) -> expression
혹은
(paramters) -> { statements; }
매개변수 목록(parameters)
메서드처럼 전달받을 인자를 정의합니다.
인자가 하나인 경우 괄호()를 생략 할 수 있습니다.
화살표 연산자 ->
람다 표현식의 매개변수와 본문(body)을 구분합니다.
본문(body)
람다가 실행할 코드를 정의합니다.
단일 표현식일 경우 중괄호{}를 생략할 수 있으며, 여러 문장일 경우 중괄호 {}로 묶습니다.
3️⃣ 예시
1. 기본적인 람다 표현식
// 기존 익명 내부 클래스 방식
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, Lambda!");
}
};
// 람다 표현식 사용
Runnable lambdaRunnable = () -> System.out.prinln("Hello, Lambda!");
위 코드에서 Runnable 인터페이스를 구현하는 익명 내부 클래스를 람다 표현식으로 대체했습니다.
Runnable은 매개변수가 없고 반환값도 없는 run() 메서드를 가지고 있기 때문에, 이를 간단하게 () ->로 표현할 수 있습니다.
2. 매개변수와 함께 사용하는 람다 표현식
// 기존 방식
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
// 람다 표현식 사용
Comparator<Integer> lambdaComparator = (o1, o2) -> o1.compareTo(o2);
위 코드에서 Comparator 인터페이스를 구현하는 익명 내부 클래스를 람다 표현식으로 대체했습니다.
두 개의 매개변수 o1과 o2를 비교하는 코드를 람다 표현식으로 간결하게 작성할 수 있습니다.
4️⃣ 함수형 인터페이스와 람다.
람다 표현식은 함수형 인터페이스를 구현하는 데 사용됩니다.
함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스로, @FuncationalInterface 애노테이션으로 명시할 수 있습니다.
예시: 함수형 인터페이스
@FunctionalInterface
interface MyFuncationalInterface {
void doSomething();
}
public class Main {
public static void main(String[] args) {
MyFunctionalInterface myFunc = () -> System.out.println("Doing something!");
myFunc.doSomething(); // 출력: Doing something!
}
}
위 코드에서 MyFunctionalInterface는 함수형 인터페이스이며, 람다 표현식 () -> System.out.println("Doing something!") 은 이 인터페이스의 doSomething() 메서드를 구현합니다.
5️⃣ 자주 사용되는 함수형 인터페이스
Java 8에서는 자주 사용되는 함수형 인터페이스들이 java.util.function 패키지에 포함되어 있습니다.
Predicate<T> : T 타입의 인자를 받아서 boolean을 반환하는 함수입니다.
Predicate<String> isEmpty = s -> s.isEmpty();
Function<T, R> : T 타입의 인자를 받아 R 타입의 결과를 반환하는 함수입니다.
Function<Integer, String> intToString = i -> Integer.toString(i);
Consumer<T> : T 타입의 인자를 받아서 소비하는(반환값이 없는) 함수입니다.
Consumer<String> print = s -> System.out.println(s);
Supplier<T> : 인자를 받지 않고 T 타입의 결과를 반환하는 함수입니다.
Supplier<Double> randomValue = () -> Math.random();
6️⃣ 장점.
1. 코드 간결화 : 람다 표현식을 사용하면 코드가 훨씬 간결해지고 가독성이 좋아집니다.
2. 지연 실행 : 람다 표현식을 통해 코드를 지연 실행할 수 있어 성능 최적화에 유리합니다.
3. 병렬 처리 : 스트림 API와 함께 사용하여 병렬 처리 작업을 쉽게 구현할 수 있습니다.
7️⃣ 요약.
람다 표현식은 익명 함수의 일종으로, 함수형 인터페이스를 구현하는 간결한 방법을 제공합니다.
이를 통해 코드의 가독성과 유지보수성이 향상되며, 함수형 프로그래밍 패러다임을 Java에서 더욱 효과적으로 활용할 수 있게 됩니다.
-
🍃[Spring] `.addAttribute()` 메서드.
🍃[Spring] .addAttribute() 메서드.
1️⃣ .addAttribute() 메서드.
.addAttribute()는 Spring MVC에서 Model 또는 ModelMap객체의 메서드로, 컨트롤러에서 뷰로 전달할 데이터를 추가하는 데 사용됩니다.
이 메서드를 사용하여 컨트롤러가 처리한 결과를 뷰에 전달하고, 뷰는 이 데이터를 사용해 동적으로 콘텐츠를 렌더링합니다.
2️⃣ 주요 기능.
모델에 데이터 추가
addAttribute()를 사용하여 키-값 쌍 형태로 데이터를 모델에 추가합니다.
이 데이터는 이후 뷰에서 사용될 수 있습니다.
뷰 랜더링에 데이터 전달
모델에 추가된 데이터는 뷰 템플릿(예: Thymeleaf, JSP)에서 접근 가능하며, 이를 통해 클라이언트에게 동적으로 생성된 HTML을 반환할 수 있습니다.
3️⃣ 사용 예시.
@Controller
public class HomeController {
@GetMapping("/home")
public String home(Model model) {
// 모델에 데이터를 추가
model.addAttribute("message", "Welcome to the Home Page!");
model.addAttribute("date", LocalDate.now());
// "home"이라는 뷰 이름을 반환
return "home";
}
}
위 코드에서 home() 메서드는 /home URL로 GET 요청이 들어오면 실행됩니다.
Model 객체를 사용하여 "message"와 "date"라는 두 개의 속성을 추가합니다.
이 속성들은 “home”이라는 뷰(예: home.html 또는 home.jsp)에서 사용됩니다.
4️⃣ 뷰에서의 사용 예시(Thymeleaf)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home Page</title>
</head>
<body>
<h1 th:text="${message}">Default Message</h1>
<p>Today's date is: <span th:text="${date}">01/01/2024</span></p>
</body>
</html>
위의 Thymeleaf 템플릿에서 ${message}와 ${date}는 컨트롤러에서 addAttribute()를 통해 추가된 데이터를 나타냅니다.
컨트롤러에서 제공한 "Welcome to the Home Page!" 와 현재 날짜가 뷰에 렌더링됩니다.
5️⃣ .addAttribute()의 다양한 사용 방법.
1. 키와 값 쌍으로 사용
가장 기본적인 사용 방법으로, 첫 번째 인수로 속성의 이름을, 두 번째 인수로 속성의 값을 지정합니다.
model.addAttribute("username", "JohnDoe");
2. 키 없이 값만 전달
키를 생략하고 값만 전달할 수도 있습니다.
이 경우 Spring은 전달된 객체의 클래스 이름을 기본 키로 사용합니다.(첫 글자는 소문자로).
User user = new User("John", "Doe");
model.addAttribute(user);
// "user"라는 키로 모델에 추가됩니다.
3. 체이닝(Chaining)
addAttribute()는 메서드 체이닝이 가능하여, 여러 개의 속성을 한 번에 추가할 수 있습니다.
model.addAttribute("name", "Jane")
.addAttribute("age", 30)
.addAttribute("city", "New York");
.addAttribute()는 Spring MVC에서 컨트롤러와 뷰 사이의 데이터를 전달하는 중요한 역할을 하며, 동적인 웹 애플리케이션 개발에 필수적인 요소입니다.
-
🍃[Spring] Spring MVC에서 `View` 객체.
🍃[Spring] Spring MVC에서 View 객체.
1️⃣ View 객체.
Spring MVC에서 View 객체는 클라이언트에게 응답을 생성하는 데 사용되는 최종 출력을 담당하는 구성 요소입니다.
View 객체는 주어진 모델 데이터를 사용하여 HTML, JSON, XML, PDF 등 다양한 형태로 응답을 렌더링 합니다.
Spring MVC의 View 객체는 추상화된 인터페이스를 통해 다양한 템플릿 엔진이나 출력 형식을 지원합니다.
2️⃣ View 객체의 주요 역할.
1. 모델 데이터 렌더링
View 객체는 컨트롤러에서 전달된 Model 데이터를 이용해 클라이언트가 볼 수 있는 형식으로 변환합니다.
예를 들어, HTML 템플릿 엔진을 사용하여 웹 페이지를 생성하거나, JSON으로 데이터를 변환해 API 응답으로 보낼 수 있습니다.
2. 응답 생성
클라이언트에게 전송될 최종 응답을 생성합니다.
이는 브라우저에 표시될 HTML 페이지일 수도 있고, RESTful API의 JSON 응답일 수도 있습니다.
3. 컨텐츠 타입 설정
View 객체는 생성된 응답 컨텐츠 타입(예: text/html, application/json, application/pdf 등)을 설정하여 클라이언트가 올바르게 해석할 수 있도록 합니다.
3️⃣ View 객체의 종류.
Spring MVC에서는 여러 가지 유형의 View 객체를 지원합니다.
이들은 다양한 응답 형식을 처리할 수 있도록 설계되었습니다.
1. JSP(JavaServer Page)
Spring MVC에서 가장 전통적으로 사용되는 뷰 기술입니다.
JSP는 HTML과 Java 코드를 혼합하여 동적인 웹 페이지를 생성합니다.
2. Thymeleaf
최근 많이 사용되는 템플릿 엔진으로, HTML 파일을 템플릿으로 사용하여, HTML 태그에 특수한 속성을 추가하여 동적인 콘텐츠를 생성할 수 있습니다.
3. FreeMarker
HTML뿐만 아니라 텍스트 기반의 다양한 문서를 생성할 수 있는 템플릿 엔진입니다.
4. Velocity
Apache 프로젝트에서 제공하는 템플릿 엔진으로, FreeMarker와 유사하게 다양한 텍스트 기반 응담을 생성할 수 있습니다.
5. JSON/XML View
MappingJackson2JsonView 와 같은 JSON 또는 XML 뷰를 사용하여 데이터를 JSON 또는 XML 형식으로 변환하여 RESTful API 응답을 생성할 수 있습니다.
6. PDF/Excel View
AbstractPdfView, AbstractXlsView 와 같은 뷰를 사용하여 PDF나 Excel 파일을 생성하여 응답으로 반환할 수 있습니다.
4️⃣ View 객체 사용 예시
컨트롤러에서 View 객체를 명시적으로 사용할 수도 있고, 뷰 리졸버(View Resolver)가 자동으로 처리할 수도 있습니다.
@Controller
public class GreetingController {
@GetMapping("/greeting")
public ModelAndView greeting() {
ModelAndView mav = new ModelAndView("greeting");
mav.addObject("message", "Hello welcome to our website!");
return mav;
}
}
위의 예제에서 ModelAndView 객체를 사용하여 View를 명시적으로 지정했습니다.
greeting이라는 뷰 이름은 보통 JSP나 Thymeleaf 템플릿 파일과 매핑되며, 이 템플릿 파일이 렌더링 됩니다.
5️⃣ 뷰 리졸버(View Resolver)
Spring MVC에서 뷰 리졸버(View Resolver)는 컨트롤러가 반환한 뷰 이름을 실제 View 객체로 변환해주는 역할을 합니다.
일반적으로 뷰 리졸버는 템플릿 엔진이나 뷰 파일의 경로를 관리하고, 클라이언트에게 응답할 적절한 View 객체를 결정합니다.
예를 들어, 다음과 같은 뷰 리졸버 설정이 있을 수 있습니다.
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
위 설정에서는 컨트롤러가 greeting이라는 뷰 이름을 반환하면, InternalResourceViewResolver는 /WEB-INF/views/greeting.jsp 라는 JSP 파일을 찾아서 렌더링합니다.
6️⃣ 요약.
View 객체는 Spring MVC에서 클라이언트에게 응답을 생성하고 렌더링하는 역할을 합니다.
이 객체는 모델 데이터를 사용하여 최종 응답을 생성하며, 다양한 형식의 출력을 지원합니다.
Spring MVC는 View 객체를 직접 사용하거나 뷰 리졸버를 통해 간접적으로 사용하여, 클라이언트에게 최종 결과를 전달합니다.
-
🍃[Spring] `@ModelAttribute` 애노테이션
🍃[Spring] @ModelAttribute 애노테이션.
1️⃣ @ModelAttribute
@ModelAttribute는 Spring Framework에서 주로 사용되는 애노테이션으로, Spring MVC에서 모델 데이터와 요청 데이터를 바인딩하거나, 컨트롤러에서 반환된 데이터를 뷰에 전당하는 데 사용됩니다.
@ModelAttribute는 두 가지 주요 용도로 사용됩니다.
1️⃣ 메서드 파라미터에서 사용(@ModelAttribute 파라미터 바인딩)
@ModelAttribute를 메서드 파라미터에 적용하면, Spring은 요청 파라미터 또는 폼 데이터로부터 객체를 자동으로 생성하고, 그 객체의 필드를 요청 파라미터 값으로 바인딩합니다.
이렇게 생성된 객체는 컨트롤러 메서드 내부에서 사용할 수 있습니다.
예시.
@Controller
public class UserController {
@PostMapping("/register")
public String registerUser(@ModelAttribute User user) {
// User 객체는 폼 데이터로부터 자동으로 생성되고 바인딩됩니다.
// 이제 user 객체를 사용할 수 있습니다.
System.out.println("User Name: " + user.getName());
return "registrationSuccess";
}
}
위 예시에서 @ModelAttribute는 User 객체를 폼 데이터로부터 생성하고, name, email 등의 필드 요청 파라미터 값으로 바인딩합니다.
2️⃣ 메서드에 사용(@ModelAttribute 메서드)
@ModelAttribute를 메서드 레벨에 적용하면, 해당 메서드는 컨트롤러의 모든 요청 전에 실행되어, 반환된 객체를 모델에 추가합니다.
이 객체는 이후 뷰에서 사용될 수 있습니다.
```java
@Controller
public class UserController {
@ModelAttribute(“userTypes”)
public List populateUserTypes() {
// 이 메서드는 컨트롤러의 모든 요청 전에 실행됩니다.
// 이 리스트는 모델에 추가되어 뷰에서 사용될 수 있습니다.
return Arrays.asList("Admin", "User", "Guest");
}
@GetMapping(“/register”)
public String showRegistrationForm(Model model) {
model.addAttribute(“user”, new User());
return “register”;
}
}
```
위 예시에서 populateUserTypes() 메서드는 @ModelAttribute("userTypes")로 선언되어, 이 메서드가 반환하는 리스트는 userTypes라는 이름으로 모델에 추가됩니다.
이 값은 뷰(예: JSP, Thymeleaf 템플릿 등)에서 접근할 수 있습니다.
3️⃣ @ModelAttribute 의 주요 기능 요약.
1. 파라미터 바인딩 : 클라이언트의 요청 데이터를 객체로 변환하고, 이 객체를 컨트롤러 메서드에서 사용하도록 해줍니다.
2. 모델 추가 : 메서드의 반환 값을 모델에 추가하여, 해당 데이터가 모든 뷰에서 사용될 수 있도록 합니다.
이 두가지 기능을 통해 @ModelAttribute는 Spring MVC에서 데이터 바인딩과 뷰에 전당되는 데이터의 관리를 간소화하는 데 중요한 역할을 합니다.
-
🍃[Spring] Spring MVC에서 `Model` 객체.
🍃[Spring] Spring MVC에서 Model 객체.
1️⃣ Model 객체.
Spring MVC에서 Model 객체는 컨트롤러와 뷰 사이에서 데이터를 전달하는 역할을 합니다.
컨트롤러에서 생성된 데이터를 Model 객체에 담아두면, 이 데이터는 뷰 템플릿(예: Thymeleaf, JSP)에서 사용될 수 있습니다.
Model 객체는 요청에 대한 응답으로 어떤 데이터를 뷰로 보내야 하는지 결정하는 데 사용됩니다.
2️⃣ Model 객체의 주요 기능.
1. 데이터 저장 및 전달.
Model 객체에 데이터를 저장하면, 해당 데이터는 뷰에 전달되어 클라이언트에게 표시됩니다.
예를 들어, 사용자의 이름이나 리스트와 같은 데이터를 뷰에 전달할 수 있습니다.
2. 키-값 쌍 형태로 데이터 관리.
Model 객체는 데이터를 키-값 쌍 형태로 관리합니다.
뷰에서 이 데이터를 사용할 때는 키를 통해 접근합니다.
3. 뷰 템플릿에서 데이터 사용.
Model에 추가된 데이터는 뷰 템플릿에서 변수로 사용됩니다.
뷰 템플릿 엔진(예: Thymeleaf, JSP)은 이 데이터를 이용해 동적인 HTML을 생성하고, 이를 클라이언트에게 반환합니다.
3️⃣ Model 객체의 사용 예.
@Controller
public class GreetingController {
@GetMapping("/greeting")
public String greeting(Model model) {
model.addAttribute("name", "John");
model.addAttribute("message", "Hello welcome to our website!");
// "greeting"이라는 뷰 이름을 반환
return "greeting";
}
}
위 예제에서 greeting() 메서드는 Model 객체에 "name" 과 "message" 라는 두 가지 데이터를 추가합니다.
이 데이터는 뷰 이름 "greeting"에 전달되며, 해당 뷰 템플릿에서 사용될 수 있습니다.
4️⃣ 뷰 템플릿에서의 사용(Thymeleaf)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Greeting Page</title>
</head>
<body>
<h1 th:text="${message}">Default Message</h1>
<p>Hello, <span th:text="${name}">User</span>!</p>
</body>
이 Thymeleaf 템플릿에서 ${message} 와 ${name} 은 Model 객체에 담긴 데이터를 사용해 동적으로 콘텐츠를 생성합니다.
컨트롤러에서 Model에 추가된 "Hello, welcome to our website!"와 "John"이 뷰에 렌더링됩니다.
5️⃣ Model, ModelMap, ModelAndView
Spring MVC에서 Model 외에도 ModelMap과 ModelAndView라는 유사한 객체들이 있습니다.
Model : 간단한 인터페이스로, 데이터 저장 및 전달을 위한 가장 기본적인 방법을 제공합니다.
ModelMap : Model의 구현체로, 데이터를 맵 형식으로 관리합니다.
ModelAndView : 모델 데이터와 뷰 이름을 함께 반환할 때 사용됩니다. 한 번에 모델과 뷰 정보를 모두 설정할 수 있습니다.
6️⃣ 요약.
Model 객체는 Spring MVC에서 컨트롤러가 뷰에 데이터를 전달하는 데 사용되는 중요한 구성 요소입니다.
이를 통해 동적인 웹 페이지를 쉽게 생성할 수 있으며, 애플리케이션의 응답 결과를 클라이언트에게 효과적으로 전달할 수 있습니다.
-
☕️[Java] 기본형 타입(Primitive Type).
☕️[Java] 기본형 타입(Primitive Type).
1️⃣ 기본형 타입(Primitive Type)란?
기본형 타입(Primitive Type)은 Java에서 가장 단순하고 기본적인 데이터 유형으로, 실제 값을 직접 저장하는 변수 유형입니다.
기본형 타입(Primitive Type)은 참조형 타입(Reference Type)과 달리 객체가 아니며, 메모리 상에 직접 데이터를 저장합니다.
Java에는 8개의 기본형 타입(Primitive Type)이 있습니다.
1️⃣ 정수형.
byte : 8비트 정수(-128 ~ 127).
short : 16비트 정수(-32,768 ~ 32,767).
int : 32비트 정수(-2^31 ~ 2^31-1).
long : 64비트 정수(-2^63 ~ 2^63-1).
2️⃣ 실수형.
float : 32비트 부동소수점.
double : 64비트 부동소수점.
3️⃣ 문자형.
char : 16비트 유니코드 문자(0 ~ 65,535)
4️⃣ 논리형.
boolean : 참 또는 거짓(true/false)
2️⃣ 기본형 타입의 한계.
1. 객체로서의 기능 부족.
기본형 타입은 객체가 아니기 때문에 메서드나 속성을 가질 수 없습니다.
이로 인해 객체지향적인 프로그래밍에서 사용되는 많은 기능을 사용할 수 없습니다.
2. 컬렉션과의 호환성 부족.
기본형 타입은 Java의 컬렉션 프레임워크(List, Set, Map 등)에 직접적으로 사용할 수 없습니다.
컬렉션은 객체만을 저장할 수 있기 때문에, 기본형을 사용할 경우 래퍼 클래스(예: Integer, Double, Boolean)로 변환해야 합니다.
3. 유연성 부족.
기본형 타입은 값이 불변이며, 추가적인 속성이나 동작을 정의할 수 없습니다.
이를 보완하기 위해 래퍼 클래스와 같은 객체가 필요합니다.
3️⃣ 기본형 타입을 사용하는 이유.
1. 성능.
기본형 타입은 참조형 타입보다 메모리 사용량이 적고, 처리 속도가 빠릅니다.
기본형 타입은 실제 값을 직접 메모리에 저장하기 때문에, 메모리 접근이 빠르고, 가비지 컬렉션과 같은 추가적인 처리 없이 직접적으로 값을 사용할 수 있습니다.
2. 메모리 효율성.
기본형 타입은 참조형 타입보다 메모리 사용량이 적습니다.
예를 들어, int는 32비트를 사용하지만, Integer는 객체를 래핑하기 때문에 추가적인 메모리 오버헤드가 발생합니다.
3. 간결함.
기본형 타입은 간단하고 사용하기 쉽습니다.
복잡한 객체 대신, 단순한 값만 필요할 때 기본형 타입을 사용하는 것이 코드의 가독성을 높이고, 불필요한 복잡성을 줄일 수 있습니다.
4. 기본적인 연산 지원.
기본형 타입은 Java 언어 내에서 산술 연산, 비교 연산 등 기본적인 연산을 직접적으로 지원합니다.
이러한 연산은 참조형 타입보다 훨씬 빠르게 수행됩니다.
4️⃣ 기본형 타입의 사용 예.
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
int sum = a + b; // 기본형 타입 간의 덧셈 연산
System.out.println("Sum: " + sum); // 출력: Sum: 30
// 기본형 타입을 사용할 경우 메모리 효율성과 성능이 우수
}
}
5️⃣ 기본형 타입과 래퍼 클래스.
Java는 기본형 타입을 객체로 감쌀 수 있는 래퍼 클래스를 제공합니다.
예를 들어, int의 래퍼 클래스는 Integer입니다.
래퍼 클래스는 기본형 타입을 객체로 변환하여 컬렉션과 같은 객체 기반 API와 호환성을 제공할 수 있습니다.
```java
public class Main {
public static void main(String[] args) {
Integer x = 10; // 오토박싱 (autoboxing)
int y = x; // 오토언박싱 (autounboxing)
// 래퍼 클래스는 메서드를 가질 수 있습니다.
int max = Integer.max(10, 20);
System.out.println(“Max: “ + max); // 출력: Max: 20
}
}
```
기본형 타입은 성능과 메모리 효율성 면에서 장점이 있지만, 객체의 유연성과 컬렉션 호환성 측면에서 한계가 있습니다.
이러한 이유로, Java에서는 상황에 따라 기본형 타입과 래퍼 클래스를 적절히 사용하게 됩니다.
-
🍃[Spring] Spring MVC에서 `Controller` 객체.
🍃[Spring] Spring MVC에서 Controller 객체.
1️⃣ Controller 객체.
Spring MVC에서 Controller 객체는 애플리케이션에서 HTTP 요청을 처리하고, 해당 요청에 대해 적절한 응답을 생성하는 역할을 하는 구성 요소입니다.
Controller는 사용자 입력을 처리하고, 비즈니스 로직을 수행한 후, 뷰를 선택하여 결과를 클라이언트에게 반환합니다.
Spring MVC에서 Controller는 주로 @Controller 또는 @RestController 애노테이션으로 정의됩니다.
1️⃣ 주요 역할.
1. HTTP 요청 처리
Controller 객체는 특정 URL 경로와 매핑된 HTTP 요청(GET, POST, PUT, DELETE 등)을 처리합니다.
각 요청은 컨트롤러의 특정 메서드와 연결되며, 메서드는 요청 매개변수를 처리하고, 필요한 작업을 수행합니다.
2. 비즈니스 로직 수행
요청을 처리하는 동안, Controller는 서비스 계층이나 비즈니스 로직을 호출하여 필요한 처리를 수행합니다.
이는 데이터베이스에서 데이터를 가져오거나, 계산을 수행하거나, 다른 복잡한 작업을 포함할 수 있습니다.
3. 모델 데이터 준비
Controller는 뷰에 전달할 데이터를 준비하고, 이를 Model 객체에 담아 뷰로 전달합니다.
이 데이터는 뷰 템플릿에서 사용되어 클라이언트에게 보여질 콘텐츠를 동적으로 생성합니다.
4. 뷰 선택 및 응답 생성
Controller는 처리 결과에 따라 어떤 뷰를 사용할지 결정합니다.
일반적으로 뷰의 이름을 반환하거나, ModelAndView 객체를 통해 뷰와 모델 데이터를 함께 반환합니다.
@RestController를 사용하면 JSON, XML 등의 형식으로 직접 데이터를 반환할 수도 있습니다.
2️⃣ Controller 객체의 정의.
Spring MVC에서 Controller는 @Controller 애노테이션으로 정의됩니다.
이 애노테이션은 클래스가 컨트롤러 역할을 한다는 것을 Spring에게 알리며, HTTP 요청을 처리할 수 있게 합니다.
예제: 기본 컨트롤러
@Controller
public class GreetingController {
@GetMapping("/greeting")
public String greeting(Model model) {
model.addAttribute("message", "Hello, welcome to our website!");
return "greeting";
}
}
위 코드에서 GreetingController 클래스는 @Controller 애노테이션을 통해 컨트롤러로 정의됩니다.
/greeting 경로로 GET 요청이 들어오면 greeting() 메서드가 호출되며, "message" 라는 데이터를 모델에 추가하고, "greeting" 이라는 뷰 이름을 반환합니다.
Spring MVC는 이 뷰 이름을 기반으로 실제 뷰를 랜더링합니다.
3️⃣ @RestController 사용.
@RestController는 @Controller와 @ResponseBody를 결합한 애노테이션입니다.
이 애노테이션이 적용된 클래스는 JSON이나 XML과 같은 데이터 형식으로 직접 응답을 반환하는 RESTful 웹 서비스의 컨트롤러로 동작합니다.
예제: REST 컨트롤러
@RestController
public class ApiController {
@GetMapping("/api/greeting")
public Map<String, String> greeting() {
Map<String, String> response = new HashMap<>();
response.put("message", "Hello, welcome to our API!");
return response;
}
}
위 코드에서 ApiController 클래스는 @RestController 애노테이션을 사용하여 정의됩니다.
/api/greeting 경로로 GET 요청이 들어오면 greeting() 메서드는 JSON 형식의 응답을 반환합니다.
4️⃣ 요청 매핑.
Controller는 URL 경로 및 HTTP 메서드와 매핑된 메서드를 통해 요청을 처리합니다.
Spring MVC는 이를 위해 @RequestMapping, @GetMapping, @PostMapping 등 다양한 매핑 애노테이션을 제공합니다.
예제: 요청 매핑.
@Controller
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String getUser(@PathVariable("id") Long id, Model model) {
User user = userService.findUserById(id);
model.addAttribute("user", user);
return "userProfile";
}
@PostMapping("/create")
public String createUser(@ModelAttribute User user) {
userService.saveUser(user);
return "redirect:/users/" + user.getId();
}
}
위 예제에서 UserController는 /users 경로와 관련된 요청을 처리합니다.
/users/{id} 경로로 GET 요청이 들어오면, 해당 사용자의 프로필 정보를 조회하여 "userProfile" 뷰에 전달합니다.
/users/create 경로로 POST 요청이 들어오면, 새로운 사용자를 생성하고, 해당 사용자의 프로필 페이지로 리다이렉트합니다.
5️⃣ 요약.
Spring MVC에서 Controller 객체는 클라이언트의 HTTP 요청을 처리하고, 비즈니스 로직을 실행한 후, 적절한 응답을 생성하는 핵심 구성 요소입니다.
Controller는 요청과 비즈니스 로직, 그리고 뷰 렌더링을 연결하는 역할을 하며, Spring MVC 애플리케이션에서 중요한 역할을 수행합니다.
-
🌐[Network] 네트워크 개념 - 프로토콜 설계 시 고려 사항
🌐[Network] 네트워크 개념 - 프로토콜 설계 시 고려 사항.
1️⃣ 프로토콜 설계 시 고려 사항.
계층 구조의 통신 프로토콜을 설계할 때는 고려할 요소가 많습니다.
대표적인 것이 네트워크 호스트의 주소 표현 방법, 데이터 전송 과정의 오류 제어, 통신 양단 사이의 전송 속도를 제어하는 흐름 제어입니다.
주소 표현은 스마트폰 번호나 IP 주소와 같이 호스트를 유일하게 구분하는 용도로 사용합니다.
오류 제어는 전송 과정에서 데이터 분실, 데이터 변형 등의 오류가 발생했을 때 데이터를 복구하는 데 사용합니다.
흐름 제어는 송신자가 데이터를 너무 빨리 보내어 수신자가 미처 처리하지 못하는 문제를 해결하기 위한 목적으로 사용합니다.
1️⃣ 주소 표현.
여러 호스트가 연결된 환경에서 특정 호스트끼리 통신하려면 상대방을 구분할 수 있는 방법이 필요합니다.
시스템을 구분하여 지칭하기 위해서 이름을 부여하는 것을 주소(Address) 체계라 합니다.
주소 체계는 시스템의 설계 과정에서 맨 먼저 고려해야 하는 중요한 개념입니다.
예를 들어, 유선 전화 시스템의 주소 표기 방법을 살펴보면 전화번호 체계는 ‘국가 코드 - 지역 코드 -번호’ 형식으로 부여됩니다.
번호 체계가 국가라는 최상위 광역 코드에서 시작해 영역을 점점 축소하는 방식으로 계층 구조에 따라 관리됩니다.
따라서 국가 코드와 지역 코드로 해당 전화기의 지리적 위치를 판단할 수 있습니다.
또 다른 주소 표현의 예로는 주민등록번호가 있습니다.
주민등록번호에는 다양한 정보가 포함되는데, yymmdd-abcdefg 형식에서 앞쪽의 yymmdd는 태어난 해의 연도, 월, 일을 의미합니다
뒤쪽의 a는 성별을 구분하는 용도로 사용되는데, 1이면 남자 2이면 여자를 의미합니다.
Y2K 문제로 인해 2000년 이후 출생자는 3이 남자, 4는 여자를 의미하도록 확장되었습니다.
즉, 1901년과 2001년에 태어난 사람은 yy로 구분하지 못하므로 a 값으로 구분해야 합니다.
주소 체계를 결정할 때는 이와 같은 확장성을 반드시 고려해야 합니다.
보통 호스트마다 주소를 하나씩 부여하지만, 다수의 호스트를 묶어 하나의 그룹 주소로 표기하기도 합니다.
이는 전통적인 통신 환경이면서 현재도 가장 많이 사용하는 일대일(1:1) 통신과 더불어, 화상 회의 등을 지원하기 위한 일대다(1:n) 통신 환경도 필요하기 때문입니다.
일대다 통신의 대표적인 유형으로는 네트워크에 연결된 모든 호스트에 데이터를 전송할 수 있는 브로드캐스팅(Broadcasting) 표기 방법과 특정 사용자를 그룹으로 묶어서 지칭하는 멀티캐스팅(Multicasting) 표기 방법이 있습니다.
예를 들어 카카오톡 앱에서 단체 전송을 하는 기능이나 줌에서 온라인 회의를 진행하는 기능이 멀티 캐스팅 표기 방법에 해당합니다.
2️⃣ 오류 제어.
네트워크에서는 데이터 송수신 과정에서 오류가 발생할 수 있습니다.
전송 오류에는 데이터가 깨져서 도착하는 데이터 변형 오류와 데이터가 도착하지 못하는 데이터 분실 오류가 있습니다.
전송 오류 문제를 해결하는 오류 제어(Error Control) 기능은 통신 프로토콜의 가장 기본적인 기능에 속합니다.
데이터 변형 오류는 물리 계층의 전송 매체에 의한 물리적인 오류이므로 전송 과정에서 데이터의 내용이 바뀌는 경우입니다.
이를 해결하기 위해서 데이터 링크 계층이 물리적인 정송 오류를 해결합니다.
물리적인 오류가 발생하지 않아도 프로토콜의 수행 과정에서 데이터를 분실하는 논리적인 전송 오류가 발생할 수 있습니다.
네트워크의 오류 제어 기능은 오류의 발생 사실을 인지하는 것이 먼저이고, 이후에 재전송 기능을 이용한 오류 복구 절차가 이어집니다.
위 그림은 송신호스트에서 보낸 데이터가 수신 호스트에 도착했을 때 발생할 수 있는 현상을 세 가지 유형으로 설명합니다.
(a)는 데이터가 오류 없이 도착하는 정상적인 경우이고, (b)는 데이터가 수신 호스트에 도착하지 못하는 데이터 분실 오류이며, (c)는 데이터 내용이 변경되어 도착하는 데이터 변형 오류입니다.
(b)처럼 데이터가 분실되는 원인은 매우 다양합니다.
전송 경로가 잘못되어 데이터가 엉뚱한 방향으로 전달되거나, 상위 계층의 논리적 처리 과정에서 데이터를 분실할 수도 있습니다.
데이터가 변형되거나 분실되는 오류가 발생되어 이를 해결하려면 먼저 오류가 발생한 사실을 인지해야 합니다.
(b)의 경우는 수신 호스트가 자신에게 데이터가 보내졌다는 사실을 인지하는 것이 쉽지 않습니다.
그래서 보통은 송신 호스트에서 타임아웃 등의 기능으로 오류를 감지하는 방법을 사용합니다.
(c)의 데이터 변형 오류는 수신 호스트가 CRC 방식 등의 오류 검출 기법을 이용해 오류를 검출할 수 있습니다.
즉, 데이터 분실 오류는 송신 호스트 주도로 이루어지고, 데이터 변형 오류는 수신 호스트 주도로 이루어집니다.
네트워크에서 전송 오류를 해결하는 일반적인 방법은 송신 호스트가 원래 데이터를 재전송(Retransmission)하는 것입니다.
물리적인 오류 외에도 통신 프로토콜에서 사용하는 알고리즘의 성격에 의해 오류가 발생하기도 합니다.
예를 들어, 송신 호스트가 순차적으로 전송한 데이터의 순서가 뒤바뀌어 도착하는 경우입니다.
수신 호스트에서 도착 순서를 바로잡으려면 데이터의 논리적인 순서를 의미하는 순서 번호 기능이 필요합니다.
3️⃣ 흐름 제어.
전송 매체에서 물리적인 오류가 없었는데도 데이터를 분실하는 경우가 있는데, 이는 송수신 호스트 사이의 데이터 전송/처리 속도 차이 때문에 발생합니다.
수신 호스트에 데이터가 도착하면 일단 내부 버퍼에 보관했다가 처리합니다.
그런데 내부 버퍼에 보관할 공간을 확보하지 못하면 데이터를 논리적으로 분실하는 결과가 초래됩니다.
일반적으로 수신 호스트의 버퍼 처리 속도보다 송신 호스트가 데이터를 전송하는 속도가 빠르면 논리적인 데이터 분실 오류가 발생합니다.
수신 호스트가 버퍼에 저장된 데이터를 미처 처리하지 못한 상태에서 새로운 데이터가 수신되어 일시적으로 저장할 공간이 없기 때문입니다.
이 문제를 해결하려면 송신 호스트의 전송 속도를 조절하는 흐름 제어(Flow Control) 기능이 필요합니다.
위 그림은 가장 단순한 형태의 흐름 제어 기법을 보여줍니다.
송신 호스트가 데이터를 전송하려면 반드시 수신 호스트로부터 명시적인 전송 허가를 받아야 합니다.
그림을 보면(a)에서 송신 호스트가 i번째 데이터를 보내고 수신 호스트가 이를 제대로 받습니다.
이후에 송신 호스트가 i+1번째 데이터를 보내려면 수신 호스트의 전송 허가가 필요합니다.
수신 호스트는 데이터를 수신할 여력이 있을 경우에만 (b)와 같이 전송 허가를 보냅니다.
(c)에서 전송 허가를 수신한 후에 송신 호스트가 다음 데이터인 i+1을 전송하고 있습니다.
보통 수신 호스트의 흐름 제어에 의해 데이터 전송이 이루어집니다.
위 그림은 흐름 제어의 이해를 돕기 위한 설명이며, 실제 통신 환경에서는 전송 속도가 너무 느리므로 이 방식이 사용되지 않습니다.
일반적으로 송신 호스트는 속도를 높이기 위하여 동시에 여러 개의 전송 데이터를 송신할 수 있으며, 수신 호스트가 데이터 개수를 통제합니다.
4️⃣ 데이터 전달 방식.
프로토콜 설계 시 고려할 마지막 사항은 데이터 전달 방식입니다.
위 그림의 (c)에서 데이터를 오른쪽이나 왼쪽의 한 방향으로만 전송하는 것을 단방향(Simplex) 방식이라 하고, (a)처럼 양쪽에서 데이터를 동시에 전송하는 것을 전이중(Full Duplex) 방식이라고 합니다.
일반 도로 환경에 비유하자면 일방통행 도로가 단방향 방식이고, 중앙에 노란 실선이 그려진 도로는 전이중 방식입니다.
일반적인 통신 프로토콜들은 모두 전이중 방식을 지원합니다.
이외에도 데이터가 양방향으로 전송되지만, 특정 시점에는 한 방향으로만 전송할 수 있는 반이중(Half Duplex) 방식이 있습니다.
위 그림에서 (b)의 반이중 방식은 양쪽에서 데이터를 동시에 전송할 수 없으므로, 데이터 전송 시점을 제어할 수 있어야 합니다.
데이터 전달 방식에서는 데이터 사이의 전송 우선순위를 설정하는 방법이나 긴급 데이터를 처리하는 방법 등도 고려해야 합니다.
-
💾 [CS] 프록시 패턴과 프록시 서버 - 2
💾 [CS] 프록시 패턴과 프록시 서버 - 2
1️⃣ 프록시 패턴(Proxy Pattern).
프록시 패턴(Proxy Pattern)은 객체 지향 프로그래밍에서 사용되는 디자인 패턴 중 하나로, 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 역할을 하는 별도의 객체(프록시, Proxy)를 제공하는 패턴입니다.
이 패턴을 통해 클라이언트는 원래의 객체 대신 프록시 객체를 통해 간접적으로 원래의 객체에 접근할 수 있습니다.
1️⃣ 프록시 패턴의 주요 목적.
1. 접근 제어 : 클라이언트가 실제 객체에 직접 접근하는 것을 제한하거나 제어할 수 있습니다.
2. 지연 초기화(Lazy Initialization) : 실제 객체의 생성과 초기화를 필요할 때까지 미룰 수 있습니다.
예를 들어, 메모리나 리소스 소모가 큰 객체를 사용하는 경우 성능을 최적화할 수 있습니다.
3. 보호(Protection) : 특정 조건에서만 실제 객체에 접근할 수 있도록 보안을 강화할 수 있습니다.
4. 원격 접근 : 원격 객체에 로컬 객체처럼 접근할 수 있도록 도와줍니다.
2️⃣ 프록시 패턴의 유형.
1. 가상 프록시(Virtual Proxy)
실제 객체의 생성을 지연시켜 성능을 최적화합니다.
예를 들어, 이미지 로딩을 지연시켜 필요할 때만 로드할 수 있습니다.
2. 보호 프록시(Protection Proxy)
접근 권한이 없는 사용자나 클라이언트가 객체에 접근하는 것을 방지합니다.
예를 들어, 특정 사용자만 큭정 기능을 사용할 수 있도록 제한할 수 있습니다.
3. 원격 프록시(Remote Proxy)
원격 서버에 위치한 객체를 로컬에서 사용하는 것처럼 보이도록 합니다.
예를 들어, 분산 시스템에서 원격 메서드를 호출할 때 사용할 수 있습니다.
4. 스마트 참조 프록시(Smart Reference Proxy)
실제 객체에 대한 추가적인 행동을 수행합니다.
예를 들어, 참조 횟수를 기록하거나, 객체에 접근할 때마다 로그를 남길 수 있습니다.
3️⃣ 프록시 패턴의 구조.
프록시 패턴은 다음과 같은 구성 요소로 이루어집니다.
Subject(주제) : 프록시와 실제 객체가 구현하는 인터페이스로, 공통된 메서드를 정의합니다.
RealSubject(실제 주제) : 실제로 작업을 수행하는 객체입니다. 클라이언트가 원래 접근하고자 하는 대상입니다.
Proxy(프록시) : RealSubject에 대한 참조를 가지고 있으며, RealSubject의 메서드를 호출하거나 접근을 제어하는 역할을 합니다.
4️⃣ 예제.
예를 들어, 이미지를 로딩하는 프로그램이 있다고 가정해봅시다.
이미지를 로드하는 작업은 시간이 오래 걸릴 수 있으므로, 이미지가 실제로 필요할 때까지 로딩을 지연시키고 싶습니다.
이때 가상 프록시를 사용할 수 있습니다.
interface Image {
void display();
}
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + filename);
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
public class ProxyPatternExample {
public static void main(String[] args) {
Image image = new ProxyImage("test.jpg");
// 실제로 이미지가 필요할 때만 로드합니다.
image.display(); // Loading test.jpg, Displaying test.jpg
image.display(); // Displaying test.jpg (이미 로드되었으므로 다시 로드하지 않습니다.)
}
}
위 코드에서 ProxyImage는 RealImage에 대한 프록시 역할을 합니다.
ProxyImage를 통해 이미지를 로드할 때, 이미지를 실제로 필요할 때만 로드하도록 지연시킬 수 있습니다.
두 번째 display() 호출에서는 이미 로드된 이미지가 있기 때문에 다시 로드하지 않습니다.
이렇게 프록시 패턴은 클라이언트의 코드 변경 없이 실제 객체의 접근 방식이나 동작을 변경하거나 확장하는 데 유용하게 사용할 수 있습니다.
5️⃣ Java에서의 프록시 패턴.
프록시 패턴(Proxy Pattern)은 Java에서도 매우 자주 사용되는 디자인 패턴 중 하나입니다.
프록시 패턴은 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 역할을 하는 별도의 객체(프록시 객체)를 제공하는 패턴입니다.
Java에서 프록시 패턴은 다양한 상황에서 사용될 수 있으며, 대표적인 예시는 다음과 같습니다.
1. 가상 프록시(Virtual Proxy)
실제 객체의 생성을 지연시키고, 필요할 때에만 객체를 생성하도록합니다.
예를 들어, 메모리나 리소스가 큰 객체의 생성을 지연시켜 성능을 향상시킬 수 있습니다.
2. 보안 프록시(Protectioon Proxy)
객체에 대한 접근을 제어하여, 특정 사용자나 조건에 따라 접근 권한을 부여하거나 제한할 수 있습니다.
예를 들어, 사용자 인증이 필요한 시스템에서 특정 서비스나 자원에 접근할 때 사용됩니다.
3. 원격 프록시(Remote Proxy)
원격 서버에 위치한 객체를 로컬에서 사용하는 것처럼 보이도록 하는 패턴입니다.
Java RMI(Remote Method Invocation)가 대표적인 예입니다.
4. 캐싱 프록시(Caching Proxy)
반복적으로 호출되는 메서드나 객체의 결과를 캐싱하여 성능을 최적화합니다.
6️⃣ Java에서 프록시 패턴을 구현하는 방법.
프록시 클래스를 직접 구현
인터페이스를 구현하는 프록시 클래스를 생성하여, 실제 객체에 대한 접근을 제어합니다.
Java Dynamic Proxy
Java의 java.lang.reflect.Proxy 클래스를 이용하여 런타임에 동적으로 프록시 객체를 생성할 수 있습니다.
이 방법은 인터페이스 기반의 프록시 생성에 유용합니다.
CGLIB
인터페이스가 없는 클래스에 대해서도 프록시를 생성할 수 있도록 지원하는 라이브러리입니다.
Spring 프레임워크에서는 주로 AOP(Aspect-Oriented Programming) 기능을 구현할 때 CGLIB을 활용합니다.
프록시 패턴은 특히 Spring 프레임워크에서 AOP를 구현하거나, 트랜잭션 관리와 같은 크로스컷팅(Cross-cutting) 관심사를 처리할 때 널리 사용됩니다.
2️⃣ 프록시 서버(Proxy Server).
프록시 서버(Proxy Server)는 컴퓨터 네트워크에서 클라이언트와 서버 간의 중계자 역할을 하는 서버입니다.
프록시 서버는 클라이언트가 요청한 리소스(웹페이지, 파일 등)를 대신 받아서 클라이언트에게 전달하거나, 클라이언트의 요청을 다른 서버로 전달합니다.
이를 통해 여러 가지 중요한 기능을 수행할 수 있습니다.
1️⃣ 프록시 서버의 주요 기능.
1. 익명성 제공 : 클라이언트의 IP 주소를 숨기고, 대신 프록시 서버의 IP 주소로 서버에 접근합니다. 이를 통해 클라이언트는 익명성을 유지할 수 있으며, 서버는 클라이언트의 실제 IP 주소를 알지 못합니다.
2. 보안 강화 : 프록시 서버는 클라이언트와 서버 간의 트래픽을 모니터링하고 제어할 수 있습니다. 이를 통해 불필요한 요청을 차단하거나, 특정 웹사이트 접근을 제한할 수 있습니다. 또한, 악성 트래픽을 필터링하여 네트워크 보안을 강화할 수 있습니다.
3. 캐싱(Caching) : 프록시 서버는 자주 요청되는 리소스를 캐싱하여 서버의 부하를 줄이고, 클라이언트의 요청에 빠르게 응답할 수 있습니다. 예를 들어, 동일한 웹페이지가 여러 번 요청되는 경우, 프록시 서버는 이 페이지를 캐시에 저장해 두고, 이후 요청에 대해 캐시에서 직접 응답합니다.
4. 콘텐츠 필터링 : 프록시 서버는 특정 콘텐츠에 대한 접근을 제한하거나 차단할 수 있습니다. 이는 학교, 기업 또는 가정에서 특정 웹사이트나 콘텐츠에 대한 접근을 제어하는 데 유용합니다.
5. 로드 밸런싱 : 프록시 서버는 여러 서버에 걸쳐 트래픽을 분산시켜 로드 밸런싱을 할 수 있습니다. 이를 통해 서버의 부하를 균등하게 나누고, 시스템의 성능과 안정성을 향상시킬 수 있습니다.
6. 데이터 압축 : 프록시 서버는 클라이언트와 서버 간에 주고받는 데이터를 압축하여 네트워크 트래픽을 줄이고, 응답 속도를 향상시킬 수 있습니다.
2️⃣ 프록시 서버의 유형.
1. 정방향 프록시(Forward Proxy)
클라이언트와 서버 사이에서 클라이언트의 요청을 대신 서버로 전달합니다.
주로 클라이언트가 프록시 서버를 통해 외부 네트워크에 접근할 때 사용됩니다.
예를 들어, 웹 브라우저 설정에서 프록시 서버를 저장하면, 모든 웹 트래픽이 이 프록시 서버를 통해 전달됩니다.
2. 리버스 프록시(Reverse Proxy)
서버와 클라이언트 사이에서 서버를 대신하여 클라이언트의 요청을 처리합니다.
주로 웹 서버 앞단에 위치하여 서버의 부하를 줄이고, 보안을 강화하며, 로드 밸런싱을 수행합니다.
리버스 프록시는 클라이언트가 실제 서버의 위치를 알지 못하게 하여 보안을 강화할 수 있습니다.
3. 웹 프록시(Web Proxy)
웹 브라우저를 통해 특정 웹사이트에 접근할 때 사용하는 프록시 서버입니다.
사용자는 웹 프록시 사이트를 통해 차단된 웹사이트에 접근하거나, 익명으로 웹을 탐색할 수 있습니다.
4. 트랜스페어런트 프록시(Transparent Proxy)
클라이언트가 프록시 서버를 사용하고 있다는 사실을 모르게 하면서 트래픽을 중계하는 프록시 서버입니다.
주로 ISP(인터넷 서비스 제공자)나 기업 네트워크에서 트레픽을 모니터링하거나 필터링하는 데 사용됩니다.
3️⃣ 프록시 서버의 예.
회사 네트워크에서의 프록시 서버
회사는 직원들이 인터넷에 접근할 때 프록시 서버를 통해 접근하도록 설정할 수 있습니다.
이 프록시 서버는 직원들이 어떤 웹사이트에 접근하는지 모니터링하고, 필요에 따라 특정 사이트에 대한 접근을 차단할 수 있습니다.
공공 Wi-Fi의 프록시 서버
공공 Wi-Fi 네트워크는 프록시 서버를 통해 사용자 트래픽을 모니터링하고, 보안을 강화할 수 있습니다.
이를 통해 악성 사이트에 대한 접근을 차단하거나, 네트워크를 통해 전송되는 데이터를 암호화할 수 있습니다.
프록시 서버는 네트워크의 성능, 보안, 그리고 사용자 경험을 개선하기 위한 강력한 도구로 사용됩니다.
4️⃣ Java에서의 프록시 서버(Proxy Server) 사용 사례.
프록시 서버는 네트워크 환경에서 클라이턴트와 서버 간의 중계자 역할을 하는 서버입니다.
Java에서는 다양한 상황에서 프록시 서버를 사용할 수 있으며, 그 활용 방법도 매우 다양합니다.
1. HTTP/HTTPS 프록시
Java 애플리케이션이 외부 네트워크에 요청을 보내야 할 때, 프록시 서버를 통해 트래픽을 중계할 수 있습니다.
이를 통해 보안, 로깅, 캐싱, IP 마스킹 등을 수행할 수 있습니다.
System.setProperty("http.proxyHost", "proxy.example.com");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "proxy.example.com");
System.setProperty("https.proxyPort", "8080");
위와 같이 시스템 프로퍼티를 설정하여 Java 애플리케이션이 HTTP 및 HTTPS 요청을 보낼 때 프록시 서버를 사용하도록 할 수 있습니다.
2. SOCKS 프록시
Java에서는 SOCKS 프록시를 사용하여 TCP/IP 기반의 모든 연결을 프록시 서버를 통해 중계할 수 있습니다.
SOCKS 프록시는 HTTP/HTTPS 프록시보다 더 일반적인 트래픽을 처리할 수 있습니다.
System.setProperty("socksProxyHost", "proxy.example.com");
System.setProperty("socksProxyPort", "1080");
이렇게 설정하면 Java 애플리케이션이 TCP/IP 연결을 시도할 때 SOCKS 프록시 서버를 통해 연결을 시도합니다.
3. RMI(Remote Method Invocation) 프록시
Java RMI는 분산 애플리케이션을 구현하기 위해 원격 메서드 호출을 가능하게 하는 기술입니다.
RMI에서 프록시를 사용하여 클라이언트가 원격 객체에 접근할 때 로컬 객체처럼 접근할 수 있도록 합니다.
4. Spring Clould Gateway
마이크로서비스 아키텍처에서 Java로 개발된 서비스들을 연결하고 API 게이트웨이 역할을 수행하는 데 Spring Clould Gateway와 같은 프록시 서버 역할을 하는 프레임워크를 사용할 수 있습니다.
이는 마이크로서비스 간의 통신을 관리하고, 보안, 로깅, 인증/인가를 담당하는 데 사용됩니다.
5. Reverse Proxy
Java 웹 애플리케이션 서버는 리버스 프록시 서버 뒤에서 실행될 수 있습니다.
예를 들어, Nginx나 Apache HTTP Server를 프록시로 설정하여 클라이언트의 요청을 Java 웹 애플리케이션으로 전달할 수 있습니다.
5️⃣ 프록시 서버 사용의 장점.
보안 강화
클라이언트와 서버 사이의 트래픽을 모니터링하고, 특정 요청을 차단하거나 허용할 수 있습니다.
캐싱
프록시 서버는 자주 요청되는 데이터를 캐시하여 서버 부하를 줄이고 응답 속도를 향상시킬 수 있습니다.
트래픽 관리
프록시 서버는 네트워크 트래픽을 관리하고, 로드 밸런싱과 같은 기능을 통해 시스템의 성능을 최적화할 수 있습니다.
IP 마스킹
클라이언트의 IP 주소를 숨기고, 프록시 서버의 IP 주소를 대신 사용할 수 있습니다.
Java 애플리케이션에서 프록시 서버를 사용하는 것은 네트워크 환경을 제어하고 보안을 강화하며 성능을 최적화하는데 매우 유용합니다.
-
💉[SQL] 데이터베이스와 DBMS
💉[SQL] 데이터베이스와 DBMS.
1️⃣ DBMS의 정의.
데이터베이스에는 우리 일상생활 대부분의 정보가 저장되고 관리됩니다.
오늘 보내거나 받은 메시지, SNS에 등록한 사진, 대중교통 이용시 찍은 교통카드, 카페에서 구매한 음료 등의 정보가 모두 데이터베이스에 기록됩니다.
데이터베이스(Database) 를 한 마디로 정의한다면 ‘데이터의 집합’이라고 할 수 있습니다.
DBMS(Database Management System)는 이런 데이터베이스를 관라하고 운영하는 소프트웨어입니다.
다양한 데이터가 저장되어 있는 데이터베이스는 여러 명의 사용자나 응용 프로그램과 공유하고 동시에 접근이 가능해야 합니다.
그래서 MS사의 엑셀과 같은 프로그램은 ‘데이터의 집합’을 관리하고 운영한다는 차원에서 DBMS로 볼 수 있지만, 대용량 데이터를 관리하거나 여러 사용자와 공유하는 개념과는 거리가 있어 DBMS라고 부르지 않습니다
(엑셀은 DBMS라고 부르지 않습니다.)
가까운 예로 은행의 예금 계좌는 많은 사람들이 가지고 있습니다.
여러 명의 예금 계좌 정보를 모아 놓은 것이 데이터베이스입니다.
은행이 가지고 있는 예금 계좌 데이터베이스에는 여러 명이 동시에 접근할 수 있습니다.
예금 계좌 주인, 은행 직원, 인터넷 뱅킹, ATM 기기 등에서 모두 접근이 가능하기 때문입니다.
이러한 것이 가능한 이유는 바로 DBMS가 있기 때문입니다.
2️⃣ DBMS의 종류.
DBMS와 같은 소프트웨어는 특정 목적을 처리하기 위한 프로그램입니다.
예를 들어 문서를 작성하기 위해서는 아래아한글(HWP)이나 워드(Word), 표 계산을 위해서는 엑셀(Excel)이나 캘크(Calc), 멋진 사진을 편집하려면 포토샵(PhotoShop)이나 김프(Gimp)와 같은 소프트웨어를 설치해야 합니다.
마찬가지로 데이터베이스를 사용하기 위해서도 소프트웨어, 즉 DBMS를 설치해야하는데 대표적으로 MySQL, 오라클(Oracle) SQL 서버(Server), MariaDB 등이 있습니다.
소프트웨어 각각의 사용 방법과 특징이 다르지만 특정 목적을 위해서는 어떤 것을 사용해도 무방합니다.
-
-
-
🌐[Network] 네트워크 개념 - 프로토콜의 이해
🌐[Network] 네트워크 개념 - 프로토콜의 이해
Preview
데이터 통신의 개념과 프로토콜의 동작 원리를 이해하는 데 스마트폰은 많은 도움을 줍니다.
스마트폰으로 전화를 거는 사람(송신자)과 받는 사람(수신자)은 서로 합의하여 통화 연결을 설정합니다
데이터 전송 단계인 통화 과정에서는 묵시적인 규칙에 따라 대화를 주고받는 일련의 절차가 진행됩니다.
스마트폰은 양쪽이 동시에 말할 수 있는 양방향 통신기능을 지원하지만, 실제로 두 사람이 동시에 말하는 경우는 거의 없고, 느낌이나 말의 내용 등에 따라 번갈아가며 대화합니다.
상대방이 한 말을 이해하지 못했거나 제대로 듣지 못한 경우에는 다시 묻는 방식을 통하여 오류를 바로 잡기도 합니다.
이와 같은 대화 과정의 절차나 대화 내용을 올바르게 이해하는 과정이 모두 프로토콜의 기능에 포함됩니다.
네트워크에서 데이터 전송 원리도 이와 비슷합니다.
통신 프로토콜에서 중심적으로 다루는 내용은 주소 개념을 포함하여 데이터 전송 오류에 관한 오류 복구 기능, 전송 속도 조절에 관한 흐름 제어 기능, 데이터의 전달 경로에 관한 라우팅 기능입니다.
스마트폰을 사용한 대화 과정에서 사람들이 무의식적으로 행하는 통화 기법이 통신 프로토콜의 설계 과정에 그대로 반영되어 있습니다.
그러나 통화 과정은 간단해 보여도 이를 통신 프로토콜로 작성하는 일은 생각보다 쉽지 않습니다.
인터넷의 동작 원리를 이해하려면 계층 구조로 설계된 OSI 7계층 모델에 대한 할습이 선행되어야 합니다.
특히, 모듈화 설계 개념의 원리와 상하 계층의 역할 분담에 대한 학습을 통하여 계층별 프로토콜의 역할을 이해해야 합니다.
인터넷에서 사용되는 데이터 전송 프로토콜인 TCP, UDP, IP 프로토콜은 계층 구조 모델의 원리에 따라 설계되었으며, 기타 제어 프로토콜의 동작도 계층 구조의 원리에 따라 이루어집니다.
1️⃣ 프로토콜의 이해
네트워크에 연결된 시스템이 통신하려면 정해진 규칙에 따라 순차적으로 데이터를 주고 받아야 하는데, 이러한 일련의 규칙을 “프로토콜(Protocol)” 이라 합니다.
프로토콜의 동작 과정은 전송 오류 여부, 데이터 전달 경로, 전송 속도 등 다양한 외부 요인의 영향을 받습니다.
따라서 적절한 대응 방안을 마련해 효율적으로 관리해야 하는데, 프로토콜의 설계 과정은 모듈(Module)화를 통하여 이루어집니다.
이렇게 함으로써 시스템의 복잡성을 단순화하고, 사용자에게 더 편리하고 간편한 통신 기능을 제공할 수 있습니다.
1️⃣ 계층적 모듈 구조
일반적으로 복잡하고 큰 시스템의 기능은 특정 단위의 모듈로 나누어 설계합니다.
각각의 모듈은 독립적으로 동작하면서도 상호 유기적으로 통합될 수 있어야 합니다.
그러므로 모듈과 모듈을 서로 연동해 동작시키는 적절한 인터페이스가 필요합니다.
모듈화
컴퓨터를 하드웨어 측면에서 보면 CPU, 메모리, 하드디스크, LAN 카드 등과 같은 작은 부품들이 모여 하나의 시스템을 구성합니다.
복잡한 시스템을 기능별로 모듈화하면 시스템 구조가 단순해져서 전체 시스템을 이해하기 쉽습니다.
또한 각 단위 모듈이 독립적인 기능을 수행하기 때문에 고장이나 업그레이드 등의 상황에 손쉽게 대처할 수 있습니다.
소프트웨어 측명에서 보면, 일반 프로그래밍 언어에서는 함수의 개념을 사용해 전체 프로그램을 모듈화할 수 있습니다.
함수별로 특정 기능을 독립적으로 수행하도록 함으로써, 각 함수가 개별적으로 설계되고 구현된다는 장점이 있습니다.
함수 사이의 인터페이스는 함수의 매개변수에 의해서만 이루어지므로 전체 시스템을 이해하기가 훨씬 쉽습니다.
즉, 함수의 역할이 매개변수로 추상화되므로 내부 구조를 이해하지 않고도 함수들을 이해할 수 있고, 이들의 모임인 전체 시스템도 쉽게 이해할 수 있습니다.
위 그림은 시스템 모듈화의 장점을 보여줍니다.
(a)처럼 전체 시스템을 기능에 따라 세 부분으로 나누어 설계할 수 있는데, 이때 각 모듈은 정해진 인터페이스에 맞게 유기적으로 연결되어야 합니다.
B 모듈은 A 모듈과 곡선 모양의 인터페이스로 연결되고, C 모듈과는 톱니 모양의 인터페이스로 연결됩니다.
A와 C 모듈은 직접 연결되는 인터페이스가 없으며, B 모듈을 통해 간접적인 관계를 유지합니다.
위 그림의 시스템을 모듈화하지 않았다면 한 부분만 고장나도 전체 시스템을 교체해야 합니다.
반면 모듈화하여 설계하면 B 모듈에 대하여 (a)의 버전 1을 (b)의 버전 2로 교체하면 됩니다.
그림에서 설명한 것과 같이 B 모듈의 동작에 오류가 확인되었거나 기능이 개선된 경우 A 모듈이나 B 모듈과 관계없이 B 모듈만 수정하는 방식입니다.
이때 모듈 내부의 처리 과정은 임의로 개선할 수 있으나, A와 C 모듈 간 인터페이스는 동일하게 유지해야 전체 시스템 동작에 영향을 주지 않습니다.
계층 구조
분할된 모듈들은 협력 관계를 유지하면서 유기적으로 동작합니다.
모듈 구조는 서로 동등한 위치에서 서비스를 주고받을 수도 있고 특정 모듈이 다른 모듈에 서비스를 제공하는 형식의 계층 구조를 이루기도 합니다.
네트워크에서는 독립적인 고유 기능을 수행하는 모듈들이 상하 계층 구조로 연결되어 동작합니다.
계층 구조에서는 위 그림에서처럼 상위 계층이 하위 계층에 특정 서비스를 요청하는 방식으로 동작합니다(1).
요청을 받은 하위 계층은 해당 서비스를 실행하여 그 결과를 상위 계층에 돌려줍니다(2).
하위 계층의 실행 결과는 상위 계층에 결과 값을 직접 전달하는 방식이 될 수도 있고, 주변 환경 값을 변경하는 부수 효과(Side Effect) 방식일 수도 있습니다.
자동차의 경우를 예로 들어봅시다.
운전자가 자동차의 속도를 줄이려면 브레이크를 밟아야 하고 브레이크를 누르는 정도에 따라 속도가 줄어듭니다.
이 구조에서 운전자는 상위 계층에 해당되며, 자동차 내부에서 속도를 줄이는 기능은 하위 계층의 모듈이 됩니다.
운전자와 감속 모듈 사이에는 브레이크라는 인터페이스가 존재합니다.
브레이크 인터페이스가 정의되면 상위 계층의 운전자가 바뀌거나 하위 계층의 자동차가 바뀌어도 둘 사이의 서비스 개념을 유지할 수 있습니다.
네트워크에서 통신하는 시스템들은 계층 구조로 모듈화된 기능이 각각 동작하며, 둘 사이의 같은 계층끼리 데이터를 전달합니다.
이때 데이터를 전달하는 규칙을 프로토콜이라 합니다.
모듈화된 계층 구조 프로토콜에는 다양한 장점이 있지만, 대표적인 장점 몇 가지를 정리하면 다음과 같습니다.
복잡하고 큰 시스템을 기능별로 작게 분류해서 간단하고 작은 시스템으로 재구성할 수 있습니다.
따라서 전체 시스템을 이해하기 쉽고, 시스템을 설계하고 구현하기도 편리합니다.
상하 계층에 인접한 모듈 사이의 인터페이스를 포함하여 분할된 모듈이 연동할 수 있는 표준 인터페이스를 제공합니다.
모듈 인터페이스는 가능하면 단순하게 구현하여 모듈들이 최대한 독립적으로 동작하도록 해야합니다.
모듈의 독립성은 전체 시스템의 구조를 단순하게 만들어줍니다.
전송 매체 양단에 있는 호스트가 수행하는 프로토콜들은 좌우 대칭 구조입니다.
대칭 구조에서는 통신 양단에 위치하는 동일 계층 사이의 프로토콜을 단순화할 수 있습니다.
각 계층의 기능 오류를 수정하거나 향상시켜야 하는 경우에 전체 시스템을 재작성하지 않고 해당 계층의 모듈만 교체하면 됩니다.
즉, 상하 혹은 좌우 계층 간의 인터페이스를 유지하면 특정 계층의 내부 변경이 다른 모듈의 동작에 영향을 미치지 않습니다.
-
☕️[Java] 메서드 체이닝 - Method Chaining
☕️[Java] 메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해 알아봅시다.
1️⃣ 예제 코드.
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다.(이 부분을 유의해서 봅시다.)
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적하여 더하고 출력합니다.
여기서는 add() 메서드의 반환값은 사용하지 않았습니다.
이번에는 add() 메서드의 반환값을 사용해봅시다.
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같습니다.
adder.add(1) 을 호출합니다.
add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다.
adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다.
add() 메서드는 자기 자신(this)의 참조값을 반환합니다.
이 반환값을 adder1, adder2, adder3에 보관했습니다.
따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다.
왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 잘 읽히지 않습니다.
이런 방식을 왜 사용할까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드에 호출에 사용해보겠습니다.
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다.
이 반환된 참조값을 변수에 담아두지 않아도 됩니다.
대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다.
다음과 같은 순서로 실행됩니다.
adder.add(1).add(2).add(3).getValue(); // value = 0
x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다.
x001.getValue(); // value = 6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다.
코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다.
마치 메서드가 체인으로 연결된 것 처럼 보입니다.
이러한 기법을 메서드 체이닝이라고 합니다.
물론 실행 결과도 기존과 동일합니다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다
예) adder.add(1), adder.add(2)
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
2️⃣ StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다.
예) insert(), delete(), reverse()
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4, 8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuild -> String
String string = sb.toString();
System.out.println("string = " + string);
}
}
위 코드를 메서드 체이닝 기법을 사용하면 아래와 같이 코드를 작성할 수 있습니다.
public class StringBuilderMain1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4, 8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
3️⃣ 정리.
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해집니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
☕️[Java] StringBuilder - 가변 String
☕️[Java] StringBuilder - 가변 String
1️⃣ 불변인 String 클래스의 단점.
불변인 String 클래스에도 단점이 있습니다.
다음 예시를 봅시다. (참고로 실제로 작동하는 코드는 아닙니다.)
"A" + "B"
String("A") + String("B") // 문자는 String 타입입니다.
String("A").concat(String("B")) // 문자의 더하기는 concat을 사용합니다.
new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다.
불변인 String의 내부 값은 변경할 수 없습니다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다.
더 많은 문자를 더하는 경우를 살펴봅시다.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
이 경우 총 3개의 String 클래스가 추가로 생성됩니다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC") 는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD") 만 사용됩니다.
결과적으로 중간에 만들어진 new String("AB"), new String("ABC)" 는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점 입니다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다.
결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고: 실제로 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
2️⃣ StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
byte[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
3️⃣ StringBuilder 사용 예시
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4, 8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuild -> String
String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성합니다.
append() 메서드를 사용해 여러 문자열을 추가합니다.
insert() 메서드로 특정 위치에 문자열을 삽입합니다.
delete() 메서드로 특정 범위의 문자열을 삭제합니다.
reverse() 메서드로 문자열을 뒤집습니다.
마지막으로 toString 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다.
실행 결과
sb = ABCD
sb = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
가변(Mutable) vs 불변(Immutable)
String은 불변합니다.
즉, 한 번 생성되면 그 내용을 변경할 수 없습니다.
따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다.
이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
반면에, StringBuilder는 가변적입니다.
하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이 때마다 새로운 객체를 생성하지 않습니다.
이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있습니다.
단 사이드 이펙트를 주의해야합니다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
-
☕️[Java] String 최적화
☕️[Java] String 최적화
1️⃣ 자바의 String 최적화.
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동을 합쳐줍니다.
문자열 리터럴 최적화.
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행합니다
(최적화 방식은 자바 버전에 따라 달라집니다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다.
대신에 문자열 더하기 연산(+)을 사용하면 충분합니다.
2️⃣ String 최적화가 어려운 경우.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화가 되기 때문이다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 1000000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 결우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000의 String 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java Hello Java...
time = 2564ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.6초가 걸렸습니다.
이럴때는 직접 StringBuilder를 사용하면 됩니다.
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java Hello Java...
time = 3ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.003초가 걸렸습니다.
3️⃣ 정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
💾 [CS] 프록시 패턴과 프록시 서버
💾 [CS] 프록시 패턴과 프록시 서버
1️⃣ 프록시 패턴
프록시 패턴(proxy pattern)은 대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정하는 등의 역할을 하는 계층에 있는 디자인 패턴입니다.
이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용합니다.
이는 앞서 설명한 프록시 객체로 쓰이기도 하지만 프록시 서버로도 활용됩니다.
용어: 프록시 서버에서의 캐싱
캐시 안에 정보를 담아두고, 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 저 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것을 말합니다.
이를 통해 불필요하게 외부와 연결하지 않기 때문에 트래픽을 줄일 수 있다는 장점이 있습니다.
2️⃣ 프록시 서버
프록시 서버(proxy server)는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킵니다.
프록시 서버로 쓰는 nginx
nginx는 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹 서버이며, 주로 Node.js 서버 앞단의 프록시 서버로 활용됩니다.
Node.js의 창시자 라인언 달은 다음과 같이 말했습니다 “Node.js의 버퍼 오버플로우 취약점을 예방하기 위해서는 nginx를 프록시 서버로 앞단에 놓고 Node.js를 뒤쪽에 놓는 것이 좋다.”라고 한 것입니다.
이러한 말은 Node.js 서버를 운영할 때 교과서처럼 참고되어 많은 사람이 이렇게 구축하고 있습니다.
Node.js 서버를 구축할 때 앞단에 nginx를 두는 것이죠.
이를 통해 익명 사용자가 직접적으로 서버에 접근하는 것을 차단하고, 간접적으로 한 단계를 더 거치게 만들어서 보안을 강화할 수 있습니다.
위의 그림처럼 nginx를 프록시 서버로 뒤서 실제 포트를 숨길 수 있고 정적 자원을 gzip 압축하거나, 메인 서버 앞단에서의 로깅을 할 수도 있습니다.
용어: 버퍼 오버플로우
버퍼는 보통 데이터가 저장되는 메모리 공간으로, 메모리 공간을 벗어나는 경우를 말합니다. 이때 사용되지 않아야 할 영역에 데이터가 덮어씌어져 주소, 값을 바꾸는 공격이 발생하기도 합니다.
용어: gzip 압축
LZ77과 Huffman 코딩의 조합인 DEFLATE 알고리즘을 기반으로 한 압축 기술입니다.
gzip 압축을 하면 데이터 전송량을 줄일 수 있지만, 압축을 해제했을 때 서버에서의 CPU 오버 헤드도 생각해서 gzip 압축 사용 유무를 결정해야 합니다.
프록시 서버로 쓰는 CloudFlare
CloudFlare는 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스입니다.
CloudFlare는 웹 서버 앞단에 프록시 서버로 두어 DDOS 공격 방어나 HTTPS 구축에 쓰입니다.
또한, 서비스를 배포한 이후에 해외에서 무안가 의심스러운 트래픽이 많이 발생하면 이 떄문에 많은 클라우드 서비스 비용이 발생할 수도 있는데, 이때 CloudFlare가 의심스러운 트래픽인지를 먼저 판단해 CAPTCHA 등을 기반으로 이를 일정부분 막아주는 역할도 수행합니다.
위의 그림처럼 사용자, 크롤러, 공격자가 자신의 웹 사이트에 접속하게 될 텐데, 이때 CloudFlare를 통해 공격자로부터 보호할 수 있습니다.
DDOS 공격 방어
DDOS는 짧은 기간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형입니다.
CloudFlare는 의심스러운 트래픽, 특히 사용자가 접속하는 것이 하닌 시스템을 통해 오는 트래픽을 자동으로 차단해서 DDOS 공격으로부터 보호합니다.
CloudFlare의 거대한 네트워크 용량과 캐싱 전략으로 소규모 DDOS 공격은 쉽게 막아낼 수 있으며 이러한 공격에 대한 방화벽 대시보드도 제공합니다.
HTTPS 구축
서버에서 HTTPS를 구축할 때 인증서를 기반으로 구축할 수도 있습니다.
하지만 CloudFlare를 사용하면 별도의 인증서 설치 없이 좀 더 손쉽게 HTTPS를 구축할 수 있습니다.
용어: CDN(Content Delivery Network)
각 사용자가 인터넷에서 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말합니다.
이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드하는 시간을 줄일 수 있습니다.
CORS와 프런트엔트의 프록시 서버
CORS(Cross-Origin Resource Sharing)는 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘입니다.
프런트엔드 개발 시 프런트엔드 서버를 만들어서 백엔드 서버와 통신할 때 주로 CROS 에러를 마주치는데, 이를 해결하기 위해 프런트엔트에서 프록시 서버를 만들기도 합니다.
용어: 오리진
프로토콜과 호스트 이름, 포트의 조합을 말합니다.
예를 들어 https://devkobe24.com:12010/test라는 주소에서 오리진은 https://devkobe24.com:12010을 뜻합니다.
예를 들어 프런트엔드에서는 127.0.0.1:3000으로 테스팅을 하는데 백엔드 서버는 127.0.0.1:12010이라면 포트 번호가 다르기 때문에 CROS 에러가 나타납니다.
이때 프록시 서버를 둬서 프런트엔드 서버에서 요청되는 오리진을 127.0.0.1:12010으로 바꾸는 것입니다.
참고로 127.0.0.1이란 루프백(loopback) IP로, 본인 IP 서버의 IP를 뜻합니다.
localhost나 127.0.0.1을 주소창에 입력하면 DNS를 거치지 않고 바로 본인 PC 서버로 연결됩니다.
위의 그림처럼 프런트엔드 서버 앞단에 프록시 서버를 놓아 /api 요청은 user API, /api2 요청은 user API2에 요청할 수 있습니다.
자연스레 CORS 에러 해결은 물론이며 다양한 API 서버와의 통신도 매끄럽게 할 수 있는 것입니다.
-
🌐[Network] 네트워크 기초 - Summary
🌐[Network] 네트워크 기초 - Summary.
1️⃣ 네트워크 기초 용어.
“네트워크” 는 전송 매체를 매개로 데이터를 교환하는 시스템의 모음입니다.
시스템과 전송 매체의 연결 지점에 대한 규격이 “인터페이스” 입니다.
시스템이 데이터를 교환할 때는 통신 규칙인 “프로토콜” 이 필요합니다.
“인터페이스” 와 “프로토콜” 은 서로 다른 시스템을 상호 연동해 동작시키기 위함이므로 반드시 “연동 형식의 통일” 이 필요하고, 이를 “표준화” 라 합니다.
“인터넷” 은 “IP” 라는 네트워크 프로토콜이 핵심적인 역할을 하는 “네트워크 집합체” 입니다.
2️⃣ 프로토콜.
상호 연동되는 시스템이 데이터를 교환할 때 사용하는 표준화된 규칙을 “프로토콜” 이라 합니다.
일반적으로 “프로토콜” 은 “동등한 위치에 있는 시스템 사이의 규칙이라는 측면이 강조” 되어 인터페이스와 구분 됩니다.
즉, 7계층 모델에서 프로토콜은 같은 계층 사이의 관계를 다루기 때문에 각각의 계층마다 서로 다른 프로토콜이 존재합니다.
일반적으로 프로토콜은 주고받은 데이터의 형식과 그 과정에서 발생하는 일련의 절차적 순서를 규정하고 있습니다.
3️⃣ 클라이언트와 서버.
“클라이언트와 서버” 의 개념은 “인터넷 서비스를 기준” 으로 “구분” 됩니다.
그 차이가 “서비스 단위” 로 이루어지므로 “임의의 호스트가 클라이언트나 서버로 고정”되지는 않습니다.
서비스를 제공하면 서버가 되고 이 서비스를 이용하면 클라이언트가 되므로 특정 서비스를 기준으로 상대적인 관점에서 클라이언트와 서버를 사용합니다.
다양한 서비스 제공을 목적으로 하는 특화된 호스트인 경우는 호스트 자체를 서버라 부르기도 합니다.
서버는 클라이언트보다 먼저 실행 상태가 되어 클라이언트의 요청에 대기해야 합니다.
4️⃣ OSI 7계층 모델.
다수의 시스템을 서로 연결해서 통신하려면 선행적으로 전체 시스템 구조를 표준화해야 합니다.
국제 표준화 단체인 ISO에서는 OSI 7계층 모델을 제안하여, 네트워크에 연결된 시스템이 갖추어야 할 기본 구조와 기능을 정의하고 있습니다.
응용 계층
표현 계층
세션 계층
전송 계층
네트워크 계층
데이터 링크 계층
물리 계층
이런 계층적인 구조로 기능을 세분화하였습니다.
일반 사용자는 응용 계층을 통해 데이터 전송을 요청하며, 이 요청은 물리 계층까지 순차적으로 전달되어 상대 호스트에 전송됩니다.
전송된 데이터는 물리 계층에서 순차적으로 응용 계층까지 전달됩니다.
5️⃣ 인터네트워킹.
네트워크와 네트워크의 연결을 인터네트워킹이라고 합니다.
인터네트워킹 기능을 수행하는 시스템을 일반적으로 게이트웨이 라 부릅니다.
게이트웨이 는 기능에 따라 종류가 다양하지만
리피터
브리지
라우터
등등..
가장 일반적인 구분 방식입니다.
리피터는 물리 계층의 기능을 지원하며, 브리지는 리피터 기능에 데이터 링크 계층의 기능이 추가된 것으로 물리 계층에서 발생한 오류를 해결해줍니다.
라우터는 물리 계층, 데이터 링크 계층, 네트워크 계층의 기능을 지원하므로 경로 선택 기능이 존재합니다.
6️⃣ 데이터 단위.
네트워크 프로토콜을 사용해 데이터를 교환할 때는 먼저 데이터를 특정 형태로 규격화하는 작업이 필요합니다.
이와 같은 한 단위의 규격으로 묶인 전송 데이터를 데이터 단위라 하며, 계층에 상관없이 호칭할 때는 통칭하여 PDU라 부릅니다.
특별히 네트워크 계층에서는 패킷, 데이터 링크 계층에서는 프레임이라는 용어가 중요하게 사용됩니다.
7️⃣ 주소의 표현.
주소의 개념은 단순히 서로를 구분한다는 고유의 목적을 넘어 주소가 가리키는 대상의 특징을 표현할 수 있습니다.
사람들은 문자로 된 이름에 익숙하지만, 0과 1로 디지털화된 환경에서는 구분자를 숫자로 된 주소로 표현할 수밖에 없습니다.
숫자로 된 주소 표현 방식은 일반 사용자에게 불편하므로 외우기 쉬운 문자 형식의 이름을 추가로 사용합니다.
인터넷에서 일반 사용자는 문자로 된 이름을 사용하고, 인터넷 내부는 숫자로 된 주소를 사용하므로 둘 사이의 변환 기능이 필요합니다.
주소에서 구분자는 유일성, 확장성, 편리성, 정보의 함축이라는 네 가지 특징을 갖습니다.
8️⃣ IP 주소.
IP 주소는 네트워크 계층의 기능을 수행하는 IP 프로토콜이 호스트를 구분하기 위하여 사용하는 주소 체계입니다.
임의의 호스트를 인터넷에 연결하려면 반드시 IP 주소를 할당받아야 합니다.
IP 주소는 32비트의 이진 숫자로 구성되는데, 보통 8비트씩 네 부분으로 나누어 십진수로 표현합니다.
IP 주소는 유일성을 보장하기 위해서 국제 표준화 기구가 전체 주소를 관리하고 할당하기 때문에 중복 주소의 사용을 원천적으로 차단합니다.
IP 주소는 임의로 할당되는 것이 아니라, 특정 규칙에 따라 인접한 주소들을 그룹으로 묶어 관리합니다.
따라서 IP 주소는 네트워크 계층에서 경로를 선택할 때 중요한 기준이 됩니다.
9️⃣ DNS 서비스
인터넷에서 호스트와 연결하려면 해당 호스트의 IP 주소를 알아야합니다.
그런데 숫자로 된 IP 주소는 기억하기 힘들어서 의미 파악이 쉬운 문자로 된 호스트 이름을 사용하는 것이 일반적입니다.
따라서 가장 먼저 수행할 작업은 DNS라는 이름과 주소 변환 기능을 이용해서 IP 주소를 얻는 것입니다.
DNS는 주소와 이름 정보를 자동으로 유지하고 관리하는 분산 데이터베이스 시스템입니다.
호스트 주소와 이름 정보는 네임 서버라는 특정한 관리 호스트가 유지하고, 주소 변환 작업이 필요한 클라이언트는 네임 서버에 요청해서 IP 주소를 얻습니다.
1️⃣0️⃣ 다양한 주소의 종류
네트워크에서 사용하는 주소는 이를 사용하는 환경에 따라 다양합니다.
OSI 7계층 모델의 각 계층에서도 목적에 따라 여러 형탱의 주소가 사용됩니다.
MAC 주소는 계층 2의 MAC 계층에서 사용하며, 일반적으로 LAN 카드에 내장되어 있습니다.
물리 계층을 통해 데이터를 전송할 때는 MAC 주소를 이용해서 호스트를 구분합니다.
IP 주소는 네트워크 계층의 기능을 수행하는 IP 프로토콜에서 사용되며, IP 패킷이 지나가는 경로를 결정하는 라우팅의 기준이 됩니다.
포트 주소는 전송 계층에서 사용하며, 호스트에서 실행되는 프로세스를 구분해줍니다.
메일 주소는 응용 계층의 메일 시스템에서 사용자를 구분하려고 사용합니다.
-
🌐[Network] 주소 정보의 관리
🌐[Network] 주소 정보의 관리.
일반 사용자가 호스트를 지칭할 때 사용하는 호스트 이름을 도메인 이름(Domain Name)이라 하며, 인터넷에서는 www.korea.co.kr과 같은 도메인 이름을 IP 주소로 변환하는 작업이 필요합니다.
초기 인터넷에서는 아주 간단한 방법으로 호스트 이름과 IP 주소를 변환하였으나, 지금은 DNS라는 분산 데이터베이스 시스템을 사용해서 보다 체계적인 방법으로 관리하고 있습니다.
1️⃣ 호스트 파일.
호스트 이름과 IP 주소를 변환하는 간단한 방법은 특정 파일(예: UNIX 시스템의 /etc/hosts)에 호스트 이름과 IP 주소의 조합을 기록하여 관리하는 것입니다.
네트워크 응용 프로그램에서는 사용자가 입력한 호스트 이름을 이 파일에서 검색하여 일대일로 대응된 IP 주소 정보를 쉽게 얻을 수 있습니다.
호스트 파일은 한 줄에 하나의 호스트 정보가 기록되며, 일반 텍스트 문서 형식으로 보관됩니다.
즉, 아래의 그림을 예로 들면 호스트 이름이 white.korea.co.kr인 시스템의 IP 주소는 211.223.201.27입니다.
네트워크 관리자는 관리 대상이 되는 모든 호스트의 이름, 주소 정보를 주기적으로 갱신하고, 이 정보를 네트워크에 연결된 모든 호스트가 복사하도록 함으로써 정보의 일관성을 유지해야 합니다.
위 그림은 네트워크 관리자가 white.korea.co.kr 에서 호스트 정보를 갱신할 때 갱신된 정보를 다른 4개의 호스트가 복사하여 저장하는 모습을 보여줍니다.
소스트 파일을 갱신하고 복사하는 작업은 보통 시스템 관리자가 수작업으로 했었습니다.
호스트가 추가되거나 삭제되면 먼저 네트워크 관리자의 호스트에서 갱신 작업이 이루어집니다
그런데 인터넷이 처음 보급되던 시기에는 호스트 파일 갱신이 생각보다 자주 발생하지 않았기 때문에 호스트 파일을 복사하는 작업도 흔하지 않았습니다.
또한 시스템 관리자가 잦은 변경을 원하지 않아서 급하지 않은 갱신은 부분적으로 늦추기도 했습니다.
하지만 지금은 DNS 서비스가 보편적으로 사용되고 있어 이처럼 호스트 파일로 관리하는 방식은 보조적으로만 사용되고 있습니다.
2️⃣ DNS
호스트 파일로 주소와 이름 정보를 관리하는 것은 간단하지만 대부분 수동으로 작업해야 한다는 단점이 있습니다.
인터넷이 확산되면서 호스트 수가 증가할수록 네트워크 관리자가 호스트 파일을 갱신하고 복사하는 작업에 많은 시간과 노력을 들여야 합니다.
특히 지금과 같이 전 세계 컴퓨터가 연결된 네트워크 환경에서는 호스트 파일로 주소와 이름을 변환하는 작업이 사실상 불가능하다고 볼 수 있습니다.
DNS(Domain Name System)는 이러한 문제점을 해결하기 위하여 고안된 것으로, 주소와 이름 정보를 자동으로 유지하고 관리하는 분산 데이터베이스 시스템입니다.
호스트 주소와 이름 정보는 네임 서버(Name Server)라는 특정한 관리 호스트가 유지하고, 주소 변환 작업이 필요한 클라이언트 네입 서버에서 요청해서 IP 주소를 얻습니다.
네트워크가 커지면 네임 서버에 보관되는 정보의 양도 자연스럽게 많아집니다.
DNS는 하나의 집중화된 네임 서버가 전체 호스트의 정보를 관리하지 않고, 여러 네임 서버에 분산하여 관리하도록 설계되었습니다.
계층 구조로 연결된 네임 서버는 자신이 관리하는 영역에 위치한 호스트 정보만 관리하며, 정보를 상호 교환하는 협력 관계를 통해서 전체 호스트 정보를 일관성 있게 유지합니다.
3️⃣ 기타 주소
네트워크에서 사용하는 주소는 이를 사용하는 환경에 따라 다양합니다.
OSI 7계층 모델의 각 계층에서도 목적에 따라 여러 형태의 주소가 사용됩니다.
인터넷에서 일반 사용자가 접할 수 있는 대표적인 주소들.
MAC 주소
MAC 주소는 계층 2의 MAC(Medium Access Protocol) 계층에서 사용하며, 일반적으로 LAN 카드에 내장되어 있습니다.
물리 계층을 통해 데이터를 전송할 때는 MAC 주소를 이용해서 호스트를 구분합니다.
따라서 네트워크 계층이 하위의 데이터 링크 계층에 데이터 전송을 요청하면 먼저 IP 주소를 MAC 주소로 변환하는 작업이 이루어지고, 이후 MAC 계층이 상대방 MAC 계층에 데이터를 전송할 수 있습니다.
IP 주소
IP 주소는 인터넷에서 네트워크 계층의 기능을 수행하는 IP 프로토콜에서 사용되며, 송신자 IP 주소와 수신자 IP 주소로 구분됩니다.
수신자 IP 주소는 IP 패킷이 지나가는 경로를 결정하는 라우팅의 기준이 됩니다.
포트 주소
포트 주소(Port Address)는 전송 계층에서 사용하며, 호스트에서 실행되는 프로세스를 구분해줍니다.
인터넷에서 연결의 완성은 호스트와 호스트 사이가 아닌, 네트워크 응용 프로세스와 네트워크 응용 프로세스 사이입니다.
예를 들어, 내 스마트폰의 메신저 앱과 상대방 스마트폰의 메신저 앱 사이의 연결이 필요하다.
이때, 하나의 IP 주소를 갖는 스마트폰에서 실행되는 여러 네트워크 응용 앱들을 구분하는 주소가 포트 주소입니다.
인터넷의 전송 계층 프로토콜인 TCP와 UDP가 독립적으로 포트 주소를 관리하며, 포트 번호 또는 소켓 주소라는 용어를 사용하기도 합니다.
메일 주소
메일 주소는 응용 계층의 메일 시스템에서 사용자를 구분하려고 사용합니다.
kobe@korea.co.kr 처럼 사용자 이름과 호스트 이름을 @ 문자로 구분해 표기합니다.
-
-
💾 [CS] 옵저버 패턴(Observer pattern)
💾 [CS] 옵저버 패턴(Observer pattern).
옵저버 패턴(observer pattern)은 주체가 어떤 객체(subject)의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴입니다.
여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 ‘추가 변화 사항’이 생기는 객체들을 의미합니다.
또한, 위의 그림처럼 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 합니다.
옵저버 패턴을 활용한 서비스로는 트위터가 있습니다.
위의 그림처럼 내가 어떤 사람인 주체를 ‘팔로우’ 했다면 주체가 포스팅을 올리게 되면 알림이 ‘팔로워’에게 가야합니다.
또한, 옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC(Model-View-Controller) 패턴에도 사용됩니다.
예를 들어 주체라고 볼 수 있는 모델(model)에서 변경 사항이 생겨 update() 메서드로 옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러(controller) 등이 작동하는 것입니다.
1️⃣ 자바에서의 옵저버 패턴.
// Observer
public interface Observer {
void update();
}
// Subject
public interface Subject {
void register(Observer obj);
void unregister(Observer obj);
void notifyObservers();
Object getUpdate(Observer obj);
}
// Topic
import java.util.ArrayList;
import java.util.List;
public class Topic implements Subject {
private List<Observer> observers;
private String message;
public Topic() {
this.observers = new ArrayList<>();
this.message = "";
}
@Override
public void register(Observer obj) {
if (!observers.contains(obj)) {
observers.add(obj);
}
}
@Override
public void unregister(Observer obj) {
observers.remove(obj);
}
@Override
public void notifyObservers() {
this.observers.forEach(Observer::update);
}
@Override
public Object getUpdate(Observer obj) {
return this.message;
}
public void postMessage(String msg) {
System.out.println("Message sended to Topic: " + msg);
this.message = msg;
notifyObservers();
}
}
// TopicSubscriber
public class TopicSubscriber implements Observer {
private String name;
private Subject topic;
public TopicSubscriber(String name, Subject topic) {
this.name = name;
this.topic = topic;
}
@Override
public void update() {
String msg = (String) topic.getUpdate(this);
System.out.println(name + ":: got message >> " + msg);
}
}
// Main
public class Main {
public static void main(String[] args) {
Topic topic = new Topic();
Observer a = new TopicSubscriber("a", topic);
Observer b = new TopicSubscriber("b", topic);
Observer c = new TopicSubscriber("c", topic);
topic.register(a);
topic.register(b);
topic.register(c);
topic.postMessage("nice to meet you");
}
}
실행 결과
Message sended to Topic: nice to meet you
a:: got message >> nice to meet you
b:: got message >> nice to meet you
c:: got message >> nice to meet you
topic을 기반으로 옵저버 패턴을 구현했습니다.
여기서 topic은 주체이자 객체가 됩니다.
class Topic implements Subject를 통해 Subject interface를 구현했고 Observer a = new TopicSubscriber("a", topic); 으로 옵저버를 선언할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정했습니다.
자바: 상속과 구현
위의 코드에 나온 implements 등 자바의 상속과 구현의 특징과 차이에 대해 알아보겠습니다.
상속(extends)
자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가 및 확장을 할 수 있는 것을 말합니다.
이로 인해 재사용성, 중복성의 최소화가 이루어집니다.
구현(Implements)
부모 인터페이스(Interface)를 자식 클래스에서 재정의하여 구현하는 것을 말합니다.
상속과는 달리 반드시 부모 클래스의 메서드를 재정의하여 구현해야 합니다.
상속과 구현의 차이
상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현합니다.
-
🍃[Spring] `@Transactional` 애노테이션
🍃[Spring] @Transactional 애노테이션.
@Transactional 애노테이션은 Spring Framework에서 제공하는 애노테이션으로, 메서드나 클래스에 적용하여 해당 범위 내의 데이터베이스 작업을 하나의 트랜잭션으로 관리할 수 있도록 해줍니다.
즉, @Transactional을 사용하면 지정된 메서드 또는 클래스 내의 데이터베이스 작업이 모두 성공해야만 커밋(commit)되고, 그렇지 않으면 롤백(rollback)됩니다.
1️⃣ 주요 기능.
1. 트랜잭션 관리.
@Transactional 애노테이션이 적용된 메서드 내에서 수행되는 모든 데이터베이스 작업(예: INSERT, UPDATE, DELETE)은 하나의 트랜잭션으로 관리됩니다.
만약 메서드 실행 중 예외가 발생하면, 해당 트랜잭션 내의 모든 변경 사항이 롤백됩니다.
2. 적용 범위.
@Transactional은 클래스나 메서드에 적용할 수 있습니다.
클래스에 적용하면 해당 클래스의 모든 메서드가 트랜잭션 내에서 실행됩니다.
메스트에 적용되면 해당 메서드만 트랜잭션으로 관리됩니다.
3. 트랜잭션 전파(Propagation)
@Transactional은 여러 전파(Propagation) 옵션을 제공하여 트랜잭션이 다른 트랜잭션과 어떻게 상호작용할지를 정의할 수 있습니다.
REQUIRED : 기본값으로, 현재 트랜잭션이 존재하면 이를 사용하고, 없으면 새로운 트랜잭션을 생성합니다.
REQUIRES_NEW : 항상 새로운 트랜잭션을 생성하고, 기존 트랜잭션을 일시 정지합니다.
MANDATORY : 현재 트랜잭션이 반드시 존재해야 하며, 없으면 예외가 발생합니다.
SUPPORT : 현재 트랜잭션이 있으면 이를 사용하고, 없으면 트랜잭션 없이 실행합니다.
기타 : NOT_SUPPORT, NEVER, NESTED 등.
4. 트랜잭션 격리 수준(Isolation Level)
데이터베이스 트랜잭션의 격리 수준을 설정할 수 있습니다.
이는 동시에 실행되는 여러 트랜잭션 간의 상호작용 방식을 정의합니다.
READ_UNCOMMITTED : 다른 트랜잭션의 미완료 변경 사항을 읽을 수 있습니다.
READ_COMMITED : 다른 트랜잭션의 커밋된 변경 사항만 읽을 수 있습니다.
REPEATABLE_READ : 트랜잭션 동안 동일한 데이터를 반복적으로 읽어도 동일한 결과를 보장합니다.
SERIALIZABLE : 가장 엄격한 격리 수준으로, 트랜잭션이 완전히 순차적으로 실행되도록 보장합니다.
5. 롤백 규칙(Rollback Rules)
기본적으로 @Transactional 은 RuntimeException 또는 Error 가 발생하면 트랜잭션을 롤백합니다.
특정 예외에 대해 롤백을 강제하거나, 롤백을 방지하도록 설정할 수 있습니다.
rollbackFor 또는 noRollbackFor 속성을 사용하여 이 동작을 커스터마이징할 수 있습니다.
6. 읽기 전용(Read-Only)
@Transactional(readOnly = true)로 설정하면 트랜잭션이 데이터 읽기 전용으로 동작하며, 이 경우 데이터 수정 작업이 최적화될 수 있습니다.
주로 SELECT 쿼리에서 사용됩니다.
2️⃣ 예시 코드
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Transactional
public void performTransaction() {
// 데이터베이스에 새로운 엔티티 추가
myRepository.save(new MyEntity("Data1"));
// 다른 데이터베이스 작업 수행
myRepository.updateEntity(1L, "UpdatedData");
// 예외 발생 시, 위의 모든 작업이 롤백됨
if (someConditionFails()) {
throw new RuntimeException("Transaction failed, rolling back...");
}
}
@Transactional(readOnly = true)
public List<MyEntity> getEntities() {
return myRepository.findAll();
}
}
3️⃣ 요약.
@Transactional 은 데이터베이스 트랜잭션을 쉽게 관리할 수 있도록 해주는 Spring의 핵심 애노테이션입니다.
이 애노테이션을 사용하면 메서드나 클래스의 데이터베이스 작업을 하나의 트랜잭션으로 처리하며, 실패 시 자동으로 롤백됩니다.
또한, 트랜잭션 전파, 격리 수준, 롤백 규칙 등을 통해 세부적으로 트랜잭션의 동작을 제어할 수 있습니다.
이러한 기능 덕분에 @Transactional은 Spring 애플리케이션에서 데이터 일관성과 무결성을 유지하는 데 중요한 역할을 합니다.
-
🍃[Spring] `@SpringBootTest` 애노테이션
🍃[Spring] @SpringBootTest 애노테이션.
@SpringBootTest 애노테이션은 Spring Boot 애플리케이션에서 통합 테스트를 수행하기 위해 사용하는 애너테이션입니다.
이 애노테이션은 테스트 클래스에서 Spring Application Context를 로드하고, 실제 애플리케이션의 전체 또는 부분적인 환경을 시뮬레이션하여 테스트할 수 있게 합니다.
주로 애플리케이션의 전반적인 기능을 테스트하거나 여러 레이어(예: 서비스, 레포지토리 등) 간의 상호작용을 검증할 때 사용됩니다.
1️⃣ 주요 기능 및 사용 방식.
1. Spring Application Context 로드
@SpringBootTest 는 기본적으로 애플리케이션의 전체 컨텍스트를 로드합니다.
이 컨텍스트는 실제 애플리케이션을 구동할 때와 동일하게 설정되어, 테스트 환경에서 애플리케이션이 어떻게 동작하는지 검증할 수 있습니다.
2. 테스트 환경 설정
@SpringBootTest 애노테이션은 다양한 속성을 통해 테스트 환경을 설정할 수 있습니다.
예를 들어, 특정 프로파일을 활성화하거나, 테스트용 설정 파일을 사용할 수 있습니다.
@SpringBootTest(properties = "spring.config.name=test-application")와 같이 속성을 지정할 수 있습니다.
3. 웹 환경 설정
@SpringBootTest는 다양한 웹 환경 모드를 지원합니다.
WebEnviroment.MOCK : 기본값으로, 웹 환경 없이 MockMvc를 사용해 서블릿 환경을 모킹합니다.
WebEnviroment.RANDOM_PORT : 테스트에 임의의 포트를 사용하여 내장 서버를 시작합니다.
WebEnviroment.DEFINED_PORT : 애플리케이션이 구성된 기본 포트를 사용합니다.
WebEnviroment.NONE : 웹 환경 없이 애플리케이션 컨텍스트만 로드합니다.
예시
@SpringBootTest(webEnvironment = SpringBootTest.WebEnviroment.RANDOM_PORT)
4. 통합 테스트
이 애노테이션은 단위 테스트와 달리, 애플리케이션의 여러 계층이 통합된 상태에서 테스트를 수행합니다.
이는 데이터베이스, 웹 서버, 서비스 레이어 등이 실제로 어떻게 상호작용하는지를 확인할 수 있게 해줍니다.
5. TestConfiguration 클래스 사용 가능
@SpringBootTest 와 함께 @TestConfiguration 을 사용하여 테스트를 위해 특별히 구성된 빈(Bean)이나 설정을 정의할 수 있습니다.
2️⃣ 예시 코드
@SpringBootTest
public class MyServiceIntegrationTest {
@Autowired
private MyService myService;
@Test
public void testServiceMethod() {
// Given
// Setup initial conditions
// When
String result = myService.performAction();
// Then
assertEquals("ExpectedResult", result);
}
}
위의 예시에서 @SpringBootTest 는 MyServiceIntegrationTest 클래스가 Spring Application Context에서 실행될 수 있도록 하고, MyService 빈이 실제로 주입되어 테스트가 실행됩니다.
3️⃣ 요약
@SpringBootTest는 Spring Boot 애플리케이션에서 통합 테스트를 쉽게 수행할 수 있도록 돕는 강력한 애노테이션입니다.
이 애노테이션을 사용하면 애플리케이션의 전체 컨텍스트를 로드한 상태에서 테스트할 수 있으며, 복잡한 애플리케이션 시나리오를 검증하는 데 유용합니다.
-
☕️[Java] JDBC(Java Database Connectivity)
☕️[Java] JDBC(Java Database Connectivity)
JDBC(Java Database Connectivity)는 자바 프로그램이 데이터베이스에 연결하고, SQL 쿼리를 실행하며, 데이터베이스로부터 결과를 가져오는 것을 가능하게 하는 자바 API입니다.
JDBC는 Java의 표준 API로, 다양한 관계형 데이터베이스 시스템(RDBMS)과의 상호 작용을 단순화하는 데 사용됩니다.
1️⃣ 주요 기능 및 개념.
1. 데이터베이스 연결
JDBC를 사용하면 자바 애플리케이션에서 데이터베이스에 연결할 수 있습니다.
이를 위해서는 데이터베이스의 URL, 사용자 이름, 비밀번호 등을 사용하여 Connection 객체를 생성합니다.
2. SQL 쿼리 실행
연결이 설정된 후, SQL 쿼리를 실행할 수 있습니다.
이는 Statement, PreparedStatment, CallableStatement 와 같은 JDBC 인터페이스를 통해 이루어집니다.
Statement는 정적 SQL 쿼리를 실행할 때 사용됩니다.
PreparedStatement는 동적 SQL 쿼리를 미리 컴파일하고, 반복 실행할 때 효율적으로 사용할 수 있습니다.
CallableStatement는 데이터베이스의 저장 프로시저를 호출할 때 사용됩니다.
3. 결과 처리
SQL 쿼리의 결과는 ResultSet 객체를 통해 얻을 수 있습니다.
ResultSet은 데이터베이스로 부터 검색된 데이터를 테이블 형식으로 제공합니다.
4. 트랜잭션 관리
JDBC는 데이터베이스 트랜젝션을 관리하는 기능도 제공합니다.
기본적으로 자동 커밋 모드이지만, 필요에 따라 수동으로 트랜잭션을 관리하고 커밋하거나 롤백할 수 있습니다.
5. 에러 처리
JDBC는 데이터베이스 관련 작업 중 발생하는 예외를 처리하기 위해 SQLException 클래스를 사용합니다.
이를 통해 에러 코드를 확인하고, 적절한 예외 처리를 할 수 있습니다.
2️⃣ JDBC의 구성 요소.
Driver
JDBC 드라이버는 특정 데이터베이스와 자바 애플리케이션 간의 통신을 담당합니다.
각 DBMS는 고유한 JDBC 드라이버를 제공합니다.
Connection
데이터베이스 연결을 표현하며, SQL 쿼리를 실행할 때 사용되는 객체를 생성합니다.
Statement
SQL 쿼리를 데이터베이스에 전달하는 역할을 합니다.
ResultSet
쿼리의 결과를 포함하며, 데이터를 읽을 수 있게 합니다.
3️⃣ JDBC의 작동 원리.
1. 드라이버 로드
애플리케이션이 사용할 JDBC 드라이버를 로드합니다.
2. 데이터베이스 연결
DriverManager.getConnection() 메서드를 사용하여 데이터베이스에 연결합니다.
3. SQL 쿼리 실행
Statement 나 PreparedStatement 객체를 사용하여 SQL 쿼리를 실행합니다.
4. 결과 처리
ResultSet 객체를 사용하여 쿼리 결과를 처리합니다.
5. 자원 해제
사용된 ResultSet, Statement, Connection 객체를 명시적으로 닫아 자원을 해제합니다.
JDBC는 데이터베이스와의 직접적인 통신을 가능하게 하며, Java 애플리케이션에서 데이터베이스 연동을 위해 널리 사용됩니다.
-
-
💾 [CS] API(Application Programming Interface)
💾 [CS] API(Application Programming Interface)
API(Application Programming Interface) 는 소프트웨어 간의 상호작용을 가능하게 해주는 인터페이스입니다.
쉽게 말해서 , API는 서로 다른 소프트웨어 시스템이나 애플리케이션이 데이터를 주고 받거나 기능을 사용할 수 있도록 도와주는 규칙과 도구들의 집합입니다.
1️⃣ API의 주요 개념.
1. 인터페이스
API는 소프트웨어 시스템이 다른 시스템이나 애플리케이션과 어떻게 소통할 수 있는지를 정의하는 인터페이스입니다.
이 인터페이스는 어떤 데이터나 기능이 노출되고, 그것들을 어떻게 사용할 수 있는지 규정합니다.
2. 추상화
API는 복잡한 시스템 내부의 구현 세부 사항을 숨기고, 사용자나 개발자가 이해하기 쉽게 필요한 기능만을 제공합니다.
예를 들어, 파일을 열거나, 데이터베이스에 쿼리를 보내거나, 웹 페이지의 데이터를 가져오는 등의 작업을 API를 통해 간단하게 수행할 수 있습니다.
3. 모듈화
API는 특정 기능이나 서비스에 대한 접근을 모듈화합니다.
이렇게 모듈화된 API를 사용하면 개발자가 시스템의 다른 부분에 영향을 주지 않고 독립적으로 기능을 사용하거나 확장할 수 있습니다.
4. 표준화
API는 표준화된 방법으로 기능에 접근할 수 있게 해주기 때문에, 여러 개발자나 시스템이 일관된 방식으로 상호작용할 수 있습니다.
예를 들어, REST API는 웹 기반 애플리케이션에서 데이터를 주고받는 표준 방식입니다.
2️⃣ API의 유형.
1. Web API(웹 API)
웹 서비스나 웹 애플리케이션에서 기능을 제공하는 API입니다.
주로 HTTP를 통해 요청과 응답을 주고받으며, REST, SOAP, GraphQL 등이 그 예입니다.
2. Library API
특정 프로그래밍 언어에서 사용할 수 있는 라이브러리나 프레임워크의 함수와 클래스들에 대한 인터페이스입니다.
예를 들어, Python의 표준 라이브러리에서 제공하는 os 나 sys 모듈도 API의 일종입니다.
3. Operating System API
운영 체제가 제공하는 기능에 접근할 수 있게 해주는 API입니다.
예를 들어, Windows API는 윈도우 애플리케이션이 운영 체제의 기능(파일 관리, UI 구성 요소, 네트워크 등)에 접근할 수 있도록 합니다.
4. Database API
데이터베이스와의 상호작용을 위해 제공되는 API입니다.
JDBC(Java Database Connectivity)는 자바 애플리케이션이 데이터베이스와 상호작용할 수 있도록 돕는 대표적인 데이터베이스 API입니다.
3️⃣ API의 예.
Google Maps API
개발자가 자신의 애플리케이션에 지도 기능을 통합할 수 있도록 Google에서 제공하는 API입니다.
Twitter API
개발자가 트위터의 기능(예: 트윗 가져오기, 트윗 작성)을 자신의 애플리케이션에 통합할 수 있도록 제공되는 API입니다.
Payment Gateway API
PayPal이나 Stripe 같은 결제 서비스에서 제공하는 API로, 애플리게이션에 결제 기능을 통합할 수 있습니다.
4️⃣ API의 중요성.
API는 소프트웨어 개발에서 매우 중요한 역할을 합니다.
그것은 소프트웨어 간의 상호 운용성을 촉진하며, 새로운 애플리케이션을 개발하거나 기존 애플리케이션에 새로운 기능을 추가하는 것을 더 쉽게 만들어 줍니다.
또한, API를 통해 외부 시스템이나 서비스와 통합할 수 있어, 다양한 기능을 제공하는 애플리케이션을 보다 효율적으로 개발할 수 있습니다.
-
🍃[Spring] 자바 코드로 직접 스프링 빈 등록하기.
🍃[Spring] 자바 코드로 직접 스프링 빈 등록하기.
스프링에서 자바 코드로 스프링 빈을 직접 등록하는 방법은 주로 @Configuration 애노테이션과 @Bean 애노테이션을 사용하여 이루어 집니다.
이 방식은 XML 설정 파일 대신 자바 클래스를 사용하여 스프링 빈을 정의하고 관리하는 방법입니다.
1️⃣ @Configuration 과 @Bean 을 사용한 빈 등록
@Configuration
이 애노테이션은 해당 클래스가 하나 이상의 @Bean 메서드를 포함하고 있으며, 스프링 컨테이너에서 빈 정의를 생성하고 처리할 수 있는 설정 클래스임을 나타냅니다.
@Bean
이 애노테이션은 메서드 레벨에서 사용되며, 메서드의 리턴값이 스프링 컨테이너에 의해 관리되는 빈(Bean)이 됨을 나타냅니다.
2️⃣ 예시.
아래는 MemoryMemberRepository 클래스를 자바 코드로 스프링 빈으로 등록하는 방법을 보여주는 예시입니다.
1. 빈으로 등록할 클래스 정의
```java
package com.devkobe.hello_spring.repository;
import com.devkobe.hello_spring.domain.Member;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
} } ```
2. 자바 설정 파일에서 빈 등록
```java
package com.devkobe.hello_spring.config;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
} } ```
3. 스프링 컨테이너에서 빈 사용
```java
package com.devkobe.hello_spring.service;
import com.devkobe.hello_spring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 비즈니스 로직 메서드들... } ```
3️⃣ 설명.
AppConfig 클래스
@Configuration 애노테이션을 사용하여 이 클래스가 스프링 설정 클래스로 사용될 것임을 명시합니다.
memberRepository() 메서드는 @Bean 애노테이션으로 정의되어 있으며, 이 메서드의 리턴값이 스프링 컨테이너에 의해 관리되는 빈이 됩니다.
이 경우, MemoryMemberRepository 객체가 빈으로 등록됩니다.
빈 사용
MemberService 클래스에서 MemberRepository 타입의 빈이 생성자 주입 방식으로 주입됩니다.
이때, AppConfig 클래스에서 등록된 MemoryMemberRepository 빈이 주입됩니다.
4️⃣ 왜 자바 설정을 사용할까?
1. 타입 안정성
자바 코드는 컴파일 시점에 타입을 체크할 수 있으므로, XML보다 타입 안정성이 높습니다.
2. IDE 지원
자바 기반 설정은 IDE의 자동 완성 기능과 리팩토링 도구를 잘 지원받을 수 있습니다.
3. 코드 재사용성
자바 설정 클래스는 일반 자바 코드처럼 재사용 사능하며, 상속과 조합 등을 활용할 수 있습니다.
5️⃣ 결론
스프링에서 자바 코드로 빈을 등록하는 방법은 @Configuration 과 @Bean 애노테이션을 사용한 방법입니다.
이 방식은 XML 기반 설정보다 더 타입 안전하고, 유지보수하기 쉬우며, 현대적인 스프링 애플리케이션에서 자주 사용됩니다.
-
🌐[Network] 주소와 이름
🌐[Network] 주소와 이름.
시스템을 지칭하는 구분자는 내부에서 처리되는 숫자 기반의 주소(Address)와 함께 사용자의 이해와 편리성을 도모하는 문자로 된 이름(Name)을 제공해야 합니다.
일반 사용자는 내부 주소를 몰라도 이름만으로 시스템에 접근할 수 있어야 하며, 이름과 주소를 연결하는 방법은 시스템 내부적으로 처리되어야 합니다.
네트워크의 규모가 크지 않아서 관리하는 시스템의 개수가 적은 경우에는 간단한 형식의 주소와 이름을 사용할 수 있으므로 이를 관리하는 시스템도 크게 복잡하지 않습니다.
그러나 관리 대상이 많아지면 주소와 이름의 공간이 커지고, 이를 관리하는 시스템의 기능도 복잡해집니다.
네트워크에는 여러 종류의 주소와 이름이 존재합니다.
이는 각 계층의 기능을 담당하는 프로토콜마다 주소를 독립적으로 관리하기 때문입니다.
예를 들어, IP 프로토콜은 호스트를 구분하기 위하여 IP 주소를 사용하며, 데이터 링크 계층에서는 LAN 카드별로 MAC 주소를 따로 부여합니다.
전송 계층을 수행하는 TCP에서는 호스트에서 수행되는 네트워크 프로세스마다 별도의 포트(Port) 주소를 할당하고 관리합니다.
1️⃣ IP 주소
IP 주소(IP Address)는 네트워크 계층의 기능을 수행하는 IP 프로토콜이 호스트를 구분하기 위하여 사용하는 주소 체계입니다.
임의의 호스트를 인터넷에 연결하려면 반드시 IP 주소를 할당 받아야 합니다.
IP 주소는 32비트의 이진 숫자로 구성되는데, 보통 8비트씩 네 부분으로 나누어 십진수로 표현합니다.
위 그림은 32비트의 이진수 11010011 11011111 11001001 00011110은 인터넷에서 사용하는 실제 IP 주소입니다.
일반 사용자는 이진수에 익숙하지 않고, 그 길이도 길어서 외우기 쉽지 않습니다.
따라서 이를 4개의 십진수로 변환한 후 각각을 점(.)으로 구분한 211.223.201.30으로 표기합니다.
이와 같은 숫자로 된 주소조차 외우기 어려우므로 문자로된 www.korea.co.kr 등의 도메인 이름을 사용합니다.
IP 주소는 유일성을 보장하기 위해서 국제 표준화 기구가 전체 주소를 관리하고 할당하기 때문에 중복 주소의 사용을 원천적으로 차단합니다.
IP 프로토콜이 처음 개발될 당시에는 현재처럼 폭넓게 활용되리라 예측하지 못했습니다.
따라서 IP 주소로 표현할 수 있는 최대 주소 공간의 크기를 32비트로 제한함으로써 확장성에 많은 문제점이 야기되고 있습니다.
이를 해결하기 위하여 새로운 프로토콜인 IPv6(Internet Protocol Version 6)에서는 주소 표현 공간을 128비트로 확장했습니다.
그리고 현재의 IP 프로토콜은 IPv6과 구분하기 위해 IPv4로 표현합니다.
IP 주소는 임의로 할당되는 것이 아니라, 특정 규칙에 따라 인접한 주소들을 그룹으로 묶어 관리합니다.
따라서 IP 주소는 네트워크 계층에서 경로를 선택할 때 중요한 기준이 됩니다.
위 그림에서 네트워크 1에는 IP 주소가 211.223.201로 시작하는 호스트들이 있고, 네트워크 2에는 211.223.202로 시작하는 호스트들이 있습니다.
왼쪽의 인터넷에서 임의의 호스트가 보낸 패킷이 중간의 라우터에 도착한 경우 이 패킷의 목적지 주소가 211.223.201.30 이라면 당연히 네트워크 1로 중개해야 합니다.
이처럼 인터넷에서 IP 주소는 패킷의 경로를 결정하는 데 중요한 역할을 합니다.
그림에 설명된 원리에 의하여, 인터넷에서 네트워크 계층 기능을 수행하는 IP 프로토콜이 전송 패킷의 경로를 결정합니다.
2️⃣ 호스트 이름
인터넷에서 특정 호스트와 연결하려면 반드시 해당 호스트의 IP 주소를 알아야 하고, 인터넷 내부의 네트워크 계층은 호스트를 IP 주소로 구분합니다.
그런데 일반 사용자는 숫자로 된 IP 주소를 기억하기 힘듭니다.
그래서 사용자들은 의미 파악이 쉬운 문자로 된 호스트 이름을 사용하는 것이 일반적입니다.
위 그림은 일반 사용자가 문자로 된 호스트 이름을 사용하였을 때 IP 주소로 변환되는 과정을 보여줍니다.
맨 밑에 있는 네트워크 계층의 IP 프로토콜은 호스트를 구분하는 용도로 IP 주소만 사용합니다.
그에 비해 일반 사용자는 IP 주소보다는 문자로 된 호스트 이름을 사용하기 때문에 중간 계층에서 이를 변환하는 기능을 수행해야 합니다.
일반적으로 FTP, 텔넷과 같은 네트워크 응용 프로그램은 실행 과정에서 사용자로부터 호스트 이름을 명령어 인수로 입력받습니다.
따라서 가장 먼저 수행할 작업은 DNS(Domain Name System)라는 이름과 주소 변환 기능을 이용해서 IP 주소를 얻는 것입니다.
이후 변환된 IP 주소의 호스트에 연결 설정이나 전송 데이터가 포함된 패킷을 전송합니다.
DNS 서비스는 호스트 이름을 , , , 라는 네 계층 구조로 나누고, 이들을 점(.)으로 구분해서 표기합니다.
예를 들어, www.korea.co.kr과 같은 호스트 이름은 대한민국(kr)에 있는 일반 회사(co) 중에서 korea라는 이름의 회사에 소속된 www라는 호스트를 의미합니다.
<호스트>.<단체 이름>.<단체 종류>.<국가 도메인>
은 가 위치한 국가의 이름을 두 글자의 약자로 표시합니다.
아래의 표처럼 나라마다 고유한 <국가 도메인이 존재합니다.
는 기관의 성격에 따라 부여하며, 사용 예는 아래의 표와 같습니다.
은 보통 단체를 상징하는 이름을 사용합니다.
예를 들어, 회사는 회사명을, 학교는 학교 이름을 사용합니다.
마지막으로 는 소속 단체의 네트워크 관리자가 내부 규칙에 따라 개별 호스테에 부여한 이름을 사용합니다.
-
☕️[Java] String 클래스 - 기본
☕️[Java] String 클래스 - 기본
1️⃣ String 클래스 - 기본
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
public class CharArrayMain {
public static void main(String[] args) {
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
System.out.println(charArr);
String str = "hello";
System.out.println("str = " + str);
}
}
실행 결과
hello
str = hello
기본형은 char 는 문자 하나를 다룰 때 사용합니다.
char 를 사용해서 여러 문자를 나열하려면 char[] 을 사용해야합니다.
하지만 이렇게 char[] 을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다.
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
쌍따옴표 사용 : "hello"
객체 생성 : new String("hello");
String 은 클래스입니다.
int, boolean 같은 기본형이 아니라 참조형입니다.
따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다.
그러므로 다음 코드는 뭔가 어색합니다.
String str1 = "hello";
문자열은 매우 자주 사용됩니다.
그래서 편의상 쌍따옴표로 문자열을 감싸면 자바언어 에서 new String("hello") 와 같이 변경해줍니다.
이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.
String str1 = "hello"; // 기존
String str1 = new String("hello"); // 변경
2️⃣ String 클래스 구조
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
클래스이므로 속성과 기능을 가집니다.
속성(필드)
private final char[] value;
여기에는 String 의 실제 문자열 값이 보관됩니다.
문자 데이터 자체는 char[] 에 보관됩니다.
String 클래스는 개발자가 직접 다루기 불편한 char[] 을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다.
그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다.
참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다.
그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다.
그래서 단순 영어, 숫자로만 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용)
그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다.
따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다.
기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다.
기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아보면 됩니다.
주요 메서드는 다음과 같습니다.
length() : 문자열의 길이를 반환합니다.
charAt(int index) : 특정 인덱스의 문자를 반환합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
3️⃣ String 클래스와 참조형.
String 은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값이 들어오는 것이 아니라 x001 과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
public class StringConcatMain {
public static void main(String[] args) {
String a = "hello";
String b = " java";
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
}
자바에서 문자열을 더할 때는 String 이 제공하는 concat() 과 같은 메서드를 사용해야 합니다.
하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공합니다.
실행 결과
result1 = hello java
result2 = hello java
-
💾 [CS] 전략 패턴(Strategy pattern)
💾 [CS] 전략 패턴(Strategy pattern)
1️⃣ 전략 패턴(Strategy pattern)
전략 패턴(Strategy pattern) 은 정책 패턴(Policy pattern) 이라고도 하며, 객채의 행위를 바꾸고 싶은 경우 ‘직접’ 수정하지 않고 전략이라고 부르는 ‘캡슐화한 알고리즘’ 을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴입니다.
아래의 예시 코드는 우리가 어떤 것을 살 때 네이버페이, 카카오페이 등 다양한 방법으로 결제하듯이 어떤 아이템을 살 때 LUNACard로 사는 것과 KAKAOCard로 사는 것을 구현한 예제입니다.
결제 방식의 ‘전략’ 만 바꿔서 두 가지 방식으로 결제하는 것을 구현했습니다.
// PaymentStrategy - interface
public interface PaymentStrategy {
void pay(int amount);
}
// KAKAOCardStrategy
public class KAKAOCardStrategy implements PaymentStrategy{
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String name, String cardNumber, String cvv, String dateOfExpiry) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.dateOfExpiry = dateOfExpiry;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using KAKAOCard.");
}
}
// LUNACardStrategy
public class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String emailId, String password) {
this.emailId = emailId;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using LUNACard");
}
}
// Item
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
// ShoppingCart
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
this.items.add(item);
}
public void removeItem(Item item) {
this.items.remove(item);
}
public int calculateTotal() {
int sum = 0;
for (Item item : items) {
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy pamentMethod) {
int amount = calculateTotal();
pamentMethod.pay(amount);
}
}
// Main
import designPattern.strategy.Item;
import designPattern.strategy.KAKAOCardStrategy;
import designPattern.strategy.LUNACardStrategy;
import designPattern.strategy.ShoppingCart;
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item A = new Item("A", 100);
Item B = new Item("B", 300);
cart.addItem(A);
cart.addItem(B);
// pay by LUNACard
cart.pay(new LUNACardStrategy("kobe@google.com", "1234"));
// pay by KAKAOCard
cart.pay(new KAKAOCardStrategy("Minseong Kang", "123456789", "123", "12/01"));
}
}
실행 결과
400 paid using LUNACard
400 paid using KAKAOCard.
위 코드는 쇼핑 카드에 아이템을 담아 LUNACard 또는 KAKAOCard 라는 두 개의 전략으로 결제하는 코드입니다.
용어 : 컨텍스트
프로그래밍에서의 컨텍스트는 상황, 맥락, 문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말합니다.
-
🍃[Spring] 스프링 컨테이너(Spring Container)란?
🍃[Spring] 스프링 컨테이너(Spring Container)란?
1️⃣ 스프링 컨테이너(Spring Container)란?
스프링 컨테이너(Spring Container)는 스프일 프레임워크의 핵심 구성 요소로, 애플리케이션에서 사용되는 객체들은 관리하고 조정하는 역할을 합니다.
이 컨테이너는 객체의 생성, 초기화, 의존성 주입, 설정 및 라이프사이클을 관리하여 애플리케이션의 주요 컴포넌트들이 잘 협력할 수 있도록 돕습니다.
스프링 컨테이너는 종종 IoC(Inversion of Control) 컨테이너 또는 DI(Dependency Injection) 컨테이너 라고도 불립니다.
2️⃣ 스프링 컨테이너의 주요 기능.
1. 빈(Bean) 관리
스프링 컨테이너는 애플리케이션에 필요한 모든 빈(Bean)을 정의하고 생성합니다.
이 빈들은 XML 설정 파일, 자바 설정 클래스, 또는 애노테이션을 통해 정의될 수 있습니다.
빈의 라이프사이클(생성, 초기화, 소멸)을 관리하고, 의존성을 자동으로 주입하여 빈 간의 결합도를 낮추어 줍니다.
2. 의존성 주입(Dependency Injection)
스프링 컨테이너는 객체 간의 의존성을 자동으로 주입하여, 객체들이 직접 다른 객체를 생성하거나 관리하지 않도록 합니다.
이를 통해 코드의 유연성과 재사용성을 높입니다.
의존성 주입은 생성자 주입, 세터 주입, 필드 주입 등 다양한 방법으로 이루어질 수 있습니다.
3. 설정 관리
컨테이너는 애플리케이션의 설정 정보를 관리합니다.
이는 빈의 정의뿐만 아니라, 데이터베이스 연결 설정, 메시지 소스, 트랜잭션 관리 등의 다양한 설정을 포함합니다.
4. 라이프사이클 인터페이스 지원
컨테이너는 빈의 라이프사이클 인터페이스(InitializingBean, DisposableBean)을 통해 빈의 초기화 및 소명 작업을 쉽게 구현할 수 있도록 지원합니다.
또한 @PostConstruct, @PreDestroy 애노테이션을 통해 라이프사이클 콜백을 간단하게 구현할 수 있습니다.
5. AOP(Aspect-Oriented Programming) 지원
스프링 컨테이너는 AOP 기능을 지원하여, 애플리케이션 전반에 걸쳐 공통적으로 사용되는 로직(예: 로깅, 트랜잭션 관리)을 비즈니스 로직과 분리하여 모듈화할 수 있게 합니다.
3️⃣ 스프링 컨테이너의 종류.
스프링에는 다양한 컨테이너 구현체가 있으며, 대표적으로 다음과 같은 종류가 있습니다.
1. BeanFactory
스프링의 가장 기본적인 컨테이너로, 빈의 기본적인 생성과 의존성 주입을 제공합니다.
하지만 BeanFactory는 지연 로딩(lazy loading) 방식으로 동작하므로, 빈이 실제로 요청될 때 생성됩니다.
2. ApplicationContext
BeanFactory의 확장판으로, 대부분의 스프링 애플리케이션에서 사용되는 컨테이너입니다.
ApplicationContext 는 BeanFactory의 기능을 포함하면서도, 다양한 기능(예: 이벤트 발행, 국제화 메시지 처리, 환경 정보 관리)을 추가로 제공합니다.
ApplicationContext 의 구현체에는 ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, AnnotationConfigApplicationContext 등이 있습니다.
4️⃣ 스프링 컨테이너의 동작 과정.
1. 빈 정의 로드
컨테이너가 시작되면, XML 파일, 자바 설정 파일, 애노테이션 등을 통해 빈의 정의를 읽어들입니다.
2. 빈 생성 및 초기화
컨테이너는 필요한 빈들을 생성하고 초기화 작업을 수행합니다.
이때 의존성이 있는 경우, 필요한 빈들을 먼저 생성하여 주입합니다.
3. 의존성 주입
빈의 생성 과정에서 필요한 의존성들이 주입됩니다.
이 과정에서 생성자 주입, 세터 주입 등이 사용됩니다.
4. 빈 제공
컨테이너는 요청 시 빈을 제공하며, 애플리케이션은 이 빈을 통해 다양한 작업을 수행할 수 있습니다.
5. 빈 소멸
애플리케이션이 종료되거나 컨테이너가 종료될 때, 컨테이너는 빈의 소멸 작업을 처리합니다.
스프링 컨테이너는 이 모든 과정을 자동으로 처리하며, 이를 통해 개발자는 비즈니스 로직에 집중할 수 있게됩니다.
-
🍃[Spring] 계층형 아키텍처(Layered Architecture), 3계층 아키텍처(Three-Tier Architecture)
🍃[Spring] 계층형 아키텍처(Layered Architecture), 3계층 아키텍처(Three-Tier Architecture)
Controller를 통해 외부의 요청을 받고, Service에서 비즈니스 로직을 처리하며, Repository에서 데이터를 저장하고 관리하는 패턴은 “계층형 아키텍처(Layered Architecture)” 또는 “3계층 아키텍처(Three-Tier Architecture)” 라고 부릅니다.
1️⃣ 계층형 아키텍처(Layered Architecture)
이 아키텍처 패턴은 애플리케이션을 여러 계층으로 나누어 각 계층이 특정한 역할을 담당하도록 구조와합니다.
이 방식은 소프트웨어의 복잡성을 줄이고, 코드의 유지보수성을 높이며, 테스트하기 쉽게 만들어줍니다.
스프링 프레임워크에서 이 패턴은 자주 사용됩니다.
2️⃣ 각 계층별 역할.
1. Presentation Layer(프레젠테이션 계층) - Controller
사용자 인터페이스와 상호작용하는 계층입니다.
외부의 요청을 받아서 처리하고, 응답을 반환합니다.
스프링에서는 주로 @Controller 또는 @RestController 를 사용하여 이 계층을 구현합니다.
2. Business Logic Layer(비즈니스 로직 계층) - Service
비즈니스 로직을 처리하는 계층입니다.
데이터의 처리, 계산, 검증 등 핵심적인 애플리케이션 로직이 구현됩니다.
스프링에서는 주로 @Service 애노테이션을 사용하여 이 계층을 구현합니다.
3. Data Access Layer(데이터 접근 계층) - Repository
데이터베이스나 외부 데이터 소스와 상호작용하는 계층입니다.
데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리합니다.
스프링에서는 주로 @Repository 애노테이션을 사용하여 이 계층을 구현하며 JPA, MyBatis, Hibernate 등의 ORM(Object-Relational Mapping) 도구와 함께 사용됩니다.
3️⃣ 이 패턴의 주요 장점.
모듈화
각 계층이 독립적으로 관리되므로, 각 계층의 코드가 명확히 분리됩니다.
유지보수성
비즈니스 로직, 데이터 접근, 그리고 프레젠테이션 로직이 분리되어 있어, 각 부분을 독립적으로 수정하거나 확장하기 쉽습니다.
테스트 용이성
각 계층을 독립적으로 테스트할 수 있어, 단위 테스트와 통합 테스트가 용이합니다.
유연성
특정 계층을 변경하거나 대체할 때, 다른 계층에 미치는 영향을 최소화할 수 있습니다.
이 계층형 아키텍처는 스프링 프레임워크를 사용하는 대부분의 애플리케이션에서 채택하는 일반적인 구조이며, 소프트웨어 설계의 베스트 프랙티스 중 하나로 널리 인정받고 있습니다.
-
🍃[Spring] 의존성 주입(Dependency Injection)을 통한 느슨한 결합(Loose Coupling) 유지.
🍃[Spring] 의존성 주입(Dependency Injection)을 통한 느슨한 결합(Loose Coupling) 유지.
아래의 코드에서 @Autowired 로 MemberService 클래스의 생성자에 MemberRepository 인터페이스를 주입받는 이유는 의존성 주입(Dependency Injection) 을 통해 느슨한 결합(Loose Coupling) 을 유지하기 위합입니다.
이는 객체지향 설계에서 매우 중요한 원칙 중 하나로, 클래스 간의 결합도를 낮춰 코드의 유연성과 확장성을 높이는 데 기여합니다.
1️⃣ 전체 코드.
// MemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
2️⃣ 구체적인 이유.
1. 인터페이스를 통한 유연성 확보.
MemberRepository 는 인터페이스로, MemoryMemberRepository 와 같은 구현체들이 이 인터페이스를 구현합니다.
인터페이스를 통해 MemberService 는 특정 구현체에 의존하지 않으며, MemberRepository 인터페이스에 의존하게 됩니다.
이는 MemberService 가 MemoryMemberRepository 나 다른 MemberRepository 구현체(JdbcMemberRepository, JpaMemberRepository 등)에 쉽게 교체될 수 있음을 의미합니다.
예를 들어, 나중에 메모리가 아닌 데이터베이스에 회원 정보를 저장하는 방식으로 전환하고 싶다면, MemoryMemberRepository 대신 새로운 구현체를 주입하면 됩니다.
2. 느슨한 결합.
MemberService 는 MemberRepository 인터페이스에만 의존하기 때문에, 어떤 구현체가 실제로 사용될지는 스프링 컨테이너가 결정합니다.
이렇게 하면 MemberService 는 구현체가 무엇인지 알 필요가 없으므로, 구현체가 변경되더라도 MemberService 를 수정할 필요가 없습니다.
이 방식은 유지보수성을 크게 향상시킵니다.
새로운 저장소 방식이 도입되더라도, 기존 비즈니스 로직에 영향을 주지 않고 새로운 기능을 추가할 수 있습니다.
3. 스프링의 의존성 주입 메커니즘 활용.
스프링은 자동으로 @Autowired 를 사용하여 적절한 MemberRepository 구현체를 찾아 주입합니다.
MemoryMemberRepository 클래스에 @Repository 애노테이션이 붙어 있기 때문에, 스프링 컨테이너는 이 클래스를 MemberRepository 타입의 빈으로 인식하고 관리하게 됩니다.
스프링은 MemberRepository 인터페이스 타입의 빈을 주입해야 하는 경우, 해당 인터페이스를 구현한 클래스 중 하나를 선택해 주입합니다.
이 예제에서는 MemoryMemberRepository 가 주입됩니다.
3️⃣ 결론.
이러한 설계는 코드의 유연성과 테스트 용이성을 크게 향상시킵니다.
인터페이스를 사용함으로써, MemberService 는 특정 구현체에 구애받지 않고 다양한 환경에서 재사용될 수 있습니다.
또한, 이는 스프링의 DI 원칙에 따라, 컴포넌트 간의 결합도를 낮추고, 애플리케이션이 변화에 잘 대응할 수 있도록 설계하는 방법입니다.
-
💾 [CS] 도메인(Domain)의 의미.
💾 [CS] 도메인(Domain)의 의미.
도메인(Domain) 은 소프트웨어 개발에서 특정 문제 영역 또는 비즈니스 영역을 지칭하는 용어입니다.
도메인은 소프트웨어 시스템이 해결하고자 하는 문제나 제공하는 서비스와 관련된 특정한 지식, 규칙, 절차 등을 포함한 모든 것을 의미합니다.
1️⃣ 도메인(Domain)의 의미.
1. 문제 영역
도메인은 특정 비즈니스나 문제 영역을 나타내며, 이 영역은 소프트웨어가 해결하려고 하는 실제 세계의 문제와 직접적으로 관련됩니다.
예를 들어, 은행 업무, 전자상거래, 병원 관리, 교육 관리 시스템 등 각각의 도메인은 서로 다른 문제와 규칙을 가지고 있습니다.
2. 도메인 지식
도메인에는 해당 문제 영역에 대한 전문 지식이나 규칙이 포함됩니다.
예를 들어, 금융 도메인에서는 이자 계산, 대출 규정, 계좌 관리와 같은 특정 지식이 중요합니다.
이와 같은 도메인 지식을 바탕으로 소프트웨어의 비즈니스 로직이 정의됩니다.
3. 도메인 모델
도메인은 일반적으로 “도메인 모델(Domain Model)”로 표현됩니다.
도메인 모델은 도메인의 개념, 객체, 엔티티, 관계, 규칙 등을 추상화하여 표현한 것입니다.
예를 들어, 은행 도메인 모델에는 고객(Customer), 계좌(Account), 거래(Transaction) 같은 객체가 포함될 수 있습니다.
도메인 모델은 시스템이 해당 도메인의 문제를 어떻게 해결할지를 정의하는데 중요한 역할을 합니다.
4. 도메인 전문가
도메인 전문가(Domain Expert)는 특정 도메인에 대한 깊은 지식을 가진 사람을 의미합니다.
이들은 비즈니스 핵심 요구 사항과 규칙을 정의하며, 개발자와 협력하여 도메인 모델을 설계하는데 중요한 역할을 합니다.
2️⃣ 도메인의 중요성
도메인은 소프트웨어 개발의 초기 단계에서 매우 중요합니다.
시스템이 해결해야 하는 문제를 명확히 정의하고, 비즈니스 요구 사항을 반영한 도메인 모델을 설계하는 것이 시스템의 성공적인 구현에 필수적입니다.
도메인 지식을 제대로 반영하지 못하면, 시스템이 실제 비즈니스 문제를 해결하는 데 실패할 수 있으며, 이는 프로젝트 실패로 이어질 수 있습니다.
따라서 개발자는 도메인 전문가와 긴밀하게 협력하여 도메인을 정확히 이해하고, 이를 코드로 표현하는 것이 중요합니다.
3️⃣ 도메인 주도 설계(Domain-Driven Design, DDD)
도메인과 관련된 중요한 소프트웨어 설계 접근법 중 하나는 도메인 주도 설계(Domain-Driven Design, DDD) 입니다.
DDD는 도메인 모델을 중심으로 소프트웨어를 설계하는 방법론으로, 도메인의 개념과 규칙을 코드에 직접 반영하여 소프트웨어의 복잡성을 관리하고, 도메인의 변화에 쉽게 적응할 수 있도록 돕습니다.
예시
예를 들어, 전자상거래 도메인 을 생각해보면, 이 도메인에는 다음과 같은 요소들이 포함될 수 있습니다.
고객(Customer) : 상품을 구매하는 사람.
상품(Product) : 고객이 구매할 수 있는 아이템.
주문(Order) : 고객이 상품을 구매할 때 생성되는 거래 기록.
결제(Payment) : 주문에 대한 대금 지불.
이러한 요소들과 그들 간의 관계가 도메인을 구성하며, 소프트웨어 시스템은 이러한 도메인의 개념을 바탕으로 비즈니스 로직을 구현하게 됩니다.
4️⃣ 결론
도메인은 소프트웨어가 다루는 문제의 범위와 관련된 개념, 규칙, 객체들을 나타내며, 이를 정확히 이해하고 모델링하는 것이 성공적인 소프트웨어 개발의 핵심입니다.
도메인 이해를 바탕으로 적절한 비즈니스 로직을 구현하는 것이 소프트웨어의 목표를 달성하는 데 매우 중요합니다.
-
💾 [CS] 비즈니스 로직(Business Logic)이란?
💾 [CS] 비즈니스 로직(Business Logic)이란?
1️⃣ 비즈니스 로직(Business Logic).
비즈니스 로직(Business Logic) 은 소프트웨어 시스템 내에서 특정 비즈니스 도메인에 대한 규칙, 계산, 절차 등을 구현한 부분을 의미합니다.
이 로직은 애플리케이션이 실제 비즈니스 요구 사항을 충족하도록 하는 핵심 기능을 담당합니다.
비즈니스 로직(Business Logic) 은 시스템이 처리해야 하는 업무 규칙과 관련된 의사결정을 포함하며, 데이터의 유효성 검사를 하고, 비즈니스 프로세스를 관리하고, 관련된 계산을 수행하는 역할을 합니다.
2️⃣ 비즈니스 로직의 주요 역할
1. 도메인 규칙 관리.
특정 비즈니스 도메인에서 따라야 하는 규칙을 정의하고 관리합니다.
예를 들어, 은행 시스템에서 계좌 이체 시 잔액이 충분해야 한다는 규칙을 비즈니스 로직에서 처리합니다.
2. 유효성 검사.
입력된 데이터나 시스템 내부에서 사용되는 데이터가 비즈니스 규칙에 맞는지 검증합니다.
예를 들어, 사용자가 입력한 주문의 총액이 0보다 큰지, 재고가 충분한지 등을 검사하는 로직이 포함됩니다.
3. 비즈니스 프로세스 구현.
비즈니스 워크플로우를 구현하여, 각 단계에서 수행해야 하는 작업을 정의하고, 순서대로 실행되도록 관리합니다.
예를 들어, 주문 처리 시스템에서 주문 접수, 결제 처리, 배송 준비 등의 단계가 비즈니스 로직에 포함될 수 있습니다.
4. 계산과 처리.
특정 비즈니스 규칙에 따라 데이터를 계산하거나 처리하는 역할을 합니다.
예를 들어, 세금 계산, 할인 적용, 이자 계산 등이 여기에 포함됩니다.
3️⃣ 비즈니스 로직의 위치
비즈니스 로직은 보통 애플리케이션의 Service 계층 에 위치합니다.
이 계층은 데이터를 처리하는 로직과 사용자 인터페이스를 담당하는 로직을 분리하여, 코드의 재사용성을 높이고 유지보수를 용이하게 합니다.
Service 계층 에서는 비즈니스 로직을 구현하며, 필요한 경우 데이터 접근 계층(Repository) 을 호출하여 데이터를 조회하거나 저장하고, 최종적으로 처리된 결과를 프레젠테이션 계층(Controller) 에 전달합니다.
4️⃣ 비즈니스 로직과 다른 로직의 구분
비즈니스 로직
실제 비즈니스와 관련된 모든 규칙과 프로세스를 정의합니다.
이는 특정 도메인 지식에 기반하며, 도메인 전문가가 주로 요구 사항을 정의합니다.
프레젠테이션 로직
사용자 인터페이스와 관련된 로직으로, 사용자에게 데이터를 표시하거나 입력을 받는 것과 관련됩니다.
데이터 접근 로직
데이터베이스와 상호작용하며, 데이터를 저장하거나 조회하는 작업을 담당합니다.
5️⃣ 비즈니스 로직의 중요성
비즈니스 로직은 애플리케이션의 핵심적인 부분이므로, 이 로직의 정확성은 시스템 전체의 신뢰성과 직결됩니다.
잘 설계된 비즈니스 로직은 애플리케이션이 요구된 비즈니스 목표를 정확히 달성할 수 있도록 돕고, 변경이 필요할 때도 쉽게 확장하거나 수정할 수 있도록합니다.
따라서 비즈니스 로직을 구현할 때는 도메인 전문가와 긴밀하게 협력하여 요구사항을 명확히 이해하고, 이를 코드로 정확히 표현하는 것이 매우 중요합니다.
-
🍃[Spring] `@Controller` 애너테이션 사용시 일어나는 일.
🍃[Spring] @Controller 애너테이션 사용시 일어나는 일.
1️⃣ 스프링 프레임워크에서 @Controller 애노테이션 사용시 어떤 일이 일어날까요?
스프링 프레임워크에서 @Controller 애노테이션을 사용하면, 해당 클래스가 스프링 MVC의 웹 컨트롤러로 동작하도록 설정됩니다.
@Controller 는 기본적으로 웹 요청을 처리하고, 적절한 응답을 생성하는 역할을 담당하는 클래스를 정의할 때 사용됩니다.
@Controller 애노테이션을 사용하면 다음과 같은 일들이 벌어집니다.
1. 스프링 빈으로 등록.
@Controller 애노테이션이 적용된 클래스는 스프링의 컴포넌트 스캔 메커니즘에 의해 자동으로 스프링 컨텍스트에 빈으로 등록됩니다.
이는 @Component 와 유사하게 동작하며, 스프링이 이 클래스를 관리하도록 만듭니다.
2. 요청 처리 메서드 매핑.
@Controller 가 달린 클래스 내의 메서드들은 @RequestMapping, @GetMapping, @PostMapping 등과 같은 요청 매핑 애노테이션을 통해 특정 HTTP 요청을 처리하는 메서드로 매핑될 수 있습니다.
이러한 매핑을 통해 특정 URL로 들어오는 요청이 어떤 메서드에 의해 처리될지 결정됩니다.
3. 모델과 뷰.
@Controller 는 주로 모델과 뷰를 처리합니다.
요청이 컨트롤러에 도달하면, 컨트롤러는 필요한 데이터를 모델에 담고, 적절한 뷰(예: JSP, Thymeleaf 템플릿)를 반환하여 사용자에게 응답을 보냅니다.
스프링은 이 작업을 쉽게 할 수 있도록 다양한 기능을 제공합니다.
4. 비즈니스 로직과 서비스 계층.
컨트롤러는 보통 직접 비즈니스 로직을 처리하지 않고, 서비스 계층을 호출하여 필요한 처리를 위임합니다.
컨트롤러는 사용자 입력을 받아 서비스로 전달하고, 서비스의 결과를 받아 사용자에게 반환하는 역할을 합니다.
5. 예외 처리.
@Controller 애노테이션을 사용하는 클래스는 또한 @ExceptionHandler 를 사용하여 특정 예외를 처리할 수 있습니다.
이를 통해 컨트롤러 내에서 발생하는 예외를 잡아 특정 응답을 반환하거나 에러 페이지를 보여줄 수 있습니다.
요약하면, @Controller 애노테이션은 해당 클래스를 스프링 MVC에서 요청을 처리하는 컨트롤러로 정의하며, HTTP 요청을 처리하고 적절한 응답을 생성하는데 중요한 역할을 합니다.
-
🍃[Spring] 빈(Bean)이란?
🍃[Spring] 빈(Bean)이란?
1️⃣ 빈(Bean)이란?
스프링 프레임워크에서 빈(Bean) 이란, 스프링 IoC(Inversion of Control) 컨테이너에 의해 관리되는 객체를 의미합니다.
스프링 빈은 애플리케이션 전반에서 사용될 수 있도록 스프링 컨텍스트에 등록된 인스턴스입니다.
빈은 보통 애플리케이션의 핵심 로직이나 비즈니스 로직을 수행하는 객체들로, 스프링은 이러한 빈들을 효율적으로 관리하고 주입합니다.
빈의 정의와 동작은 스프링의 핵심 개념인 의존성 주입(Dependency Injection, DI) 과 밀접한 관련이 있습니다.
2️⃣ 스프링 빈의 주요 특징.
1. 싱글톤(Singleton) 스코프
기본적으로 스프링 빈은 싱글톤 스코프로 관리됩니다.
즉, 특정 빈 타입에 대해 스프링 컨테이너는 하나의 인스턴스만을 생성하고 애플리케이션 내에서 재사용합니다.
물론, 필요에 따라 프로토타입, 요청, 세션 등 다른 스코프로 빈을 정의할 수도 있습니다.
2. 의존성 관리
스프링 컨테이너는 빈의 의존성을 자동으로 주입합니다.
즉, 빈이 생성될 때 필요한 의존성(다른 빈이나 리소스)을 스프링이 자동으로 주입해줍니다.
이 과정에서 생성자 주입, 세터 주입, 필드 주입 등 다양한 방법이 사용될 수 있습니다.
3. 라이프사이클 관리
스프링은 빈의 생성부터 소멸까지의 라이프사이클을 관리합니다.
빈이 생성될 때 초기화 작업을 하거나, 빈이 소멸될 때 클린업 작업을 수행할 수 있도록 다양한 훅(Hook)을 제공하며, 이 과정에서 @PostConstruct, @PreDestroy 같은 애노테이션을 사용할 수 있습니다.
4. 설정 및 구성
빈은 XML 설정 파일이나 자바 설정 클래스에서 정의될 수 있습니다.
또한, @Component, @Service, @PostConstruct, @PreDestroy 같은 애노테이션을 사용할 수 있습니다.
5. 느슨한 결합(Loose Coupling)
스프링 빈을 사용하면 객체 간의 의존성을 직접 설정하는 것이 아니라, 스프링이 관리하므로 코드가 더욱 유연하고 테스트하기 쉬워집니다.
이는 애플리케이션의 유지보수성과 확장성을 높여줍니다.
3️⃣ 스프링 빈의 정의 예시.
다음은 빈이 어떻게 정의되고, 스프링 컨테이너가 이를 관리하는지에 대한 간단한 예시입니다.
@Component
public class MyService {
public void performService() {
System.out.println("Service is being performed.")
}
}
위 코드에서 @Component 애노테이션이 적용된 MyService 클래스는 스프링 빈으로 등록됩니다.
스프링 컨테이너는 이 빈을 관리하고, 필요할 때 의존성을 주입합니다.
빈을 스프링 컨텍스트에서 가져와 사용하는 예시는 다음과 같습니다.
```java
@Autowired
private MyService myService;
public void useService() {
myService.performService();
}
```
여기서 @Autowired 애노테이션은 MyService 타입의 빈을 스프링 컨테이너에서 주입받아 useService 메서드에서 사용할 수 있도록 합니다.
결론적으로, 스프링 빈은 스프링 애플리케이션에서 핵심적인 역할을 하는 객체로, 스프링 컨테이너가 관리하는 인스턴스이며, 이를 통해 애플리케이션의 구성 요소들이 유연하고 효율적으로 동작하도록 돕습니다.
-
-
-
☕️[Java] 불변 객체 - 문제와 풀이
☕️[Java] 불변 객체 - 문제와 풀이
1️⃣ 불변 객체 - 문제와 풀이.
문제 설명
MyDate 클래스는 불변이 아니어서 공유 참조시 사이드 이펙트가 발생합니다. 이를 불변 클래스로 만들어야 합니다.
새로운 불변 클래스는 ImmutableMyDate
새로운 실행 클래스는 ImmutableMyDateMain
1️⃣ 불변이 아닌 MyDate 클래스
// MyDate
public class MyDate {
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void setYear(int year) {
this.year = year;
}
public void setMonth(int month) {
this.month = month;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// MyDateMain
public class MyDateMain {
public static void main(String[] args) {
MyDate date1 = new MyDate(2024, 9, 1);
MyDate date2 = date1;
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
System.out.println("2025 -> date1");
date1.setYear(2025);
System.out.println("date1 = " + date1);
System.out.println("date2 = " + date2);
}
}
실행 결과
date1 = 2024-9-1
date2 = 2024-9-1
2025 -> date1
date1 = 2025-9-1
date2 = 2025-9-1
2️⃣ 불변 클래스인 ImmutableMyDate
// ImmutableMyDate
public class ImmutableMyDate {
private final int year;
private final int month;
private final int day;
public ImmutableMyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public ImmutableMyDate withYear(int newYear) {
return new ImmutableMyDate(newYear, month, day);
}
public ImmutableMyDate withMonth(int newMonth) {
return new ImmutableMyDate(year, newMonth, day);
}
public ImmutableMyDate withDay(int newDay) {
return new ImmutableMyDate(year, month, newDay);
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
// ImmutableMyDateMain
public class ImmutableMyDateMain {
public static void main(String[] args) {
ImmutableMyDate immutableDate1 = new ImmutableMyDate(2024, 9, 1);
ImmutableMyDate immutableDate2 = immutableDate1;
System.out.println("immutableDate1 = " + immutableDate1);
System.out.println("immutableDate2 = " + immutableDate2);
System.out.println("2025 -> immutableDate1");
// 방법 1.
//immutableDate1 = new ImmutableMyDate(2025, 9, 1);
// 방법 2. -> 이 방법을 더 지향함
// 주의: 불변 객체에서 값을 변경하는 메서드가 있을 경우에는 무조건 반환값을 받아서 참조를 가지고 가야 바뀐 값을 사용할 수 있습니다.
immutableDate1 = immutableDate1.withYear(2025); // x002
System.out.println("immutableDate1 = " + immutableDate1); // x002
System.out.println("immutableDate2 = " + immutableDate2); // x001
}
}
실행 결과
immutableDate1 = 2024-9-1
immutableDate2 = 2024-9-1
2025 -> immutableDate1
immutableDate1 = 2025-9-1
immutableDate2 = 2024-9-1
3️⃣ 참고 - withXxx()
불변 객체에서 값을 변경하는 경우 withYear() 처럼 “with” 로 시작하는 경우가 많습니다.
예를 들어, “coffee with sugar” 라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든가는 것을 의미합니다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 “with” 로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻합니다.
정리하면 “with” 는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현합니다.
-
🌐[Network] 주소의 표현
🌐[Network] 주소의 표현.
시스템을 설계할 때는 기능이나 목적과 함께 고유의 구분자(Identifier)를 부여하는 방법에 대해서도 우선하여 고려해야 합니다.
일반적으로 주소의 개념은 단순히 서로를 구분한다는 고유 목적을 넘어서 주소가 가리키는 대상의 특징을 표현할 수 있습니다.
사람들은 문자로 된 이름에 익숙하지만, 0과 1로 디지털화된 환경에서는 구분자를 숫자로 된 주소로 표현할 수밖에 없습니다.
디지털 환경에서 숫자로 된 주소 표현 방식은 일반 사용자에게 불편하므로 보통은 외우기 쉬운 문자 형식의 이름을 추가로 사용합니다.
주소와 이름은 일대일(1:1) 관계가 이루어지며, 이들은 연결하는 기능이 필요합니다.
인터넷에서 일반 사용자는 문자로 된 이름을 사용하고, 인터넷 내부는 숫자로 된 주소를 사용하므로 둘 사이의 변환 기능이 필요합니다.
대상을 유일하게 구별하는 구분자는 일반적으로 다음의 네 가지 특징이 있습니다.
1️⃣ 유일성
구분자의 가장 중요한 역할은 대상을 서로 구분하여 지칭하는 것입니다.
따라서 서로 다른 대상이 같은 구분자를 갖지 않는 유일성을 보장해야 합니다.
그러나 이론적으로 완전한 확장성을 전제로 하는 유일성을 보장하기는 불가능합니다.
예를 들어, 주민 번호에서 앞쪽 여섯 글자인 생년월일은 100년 이내에 출생한 사람들만 구분할 수 있습니다.
현재는 방편적으로 바로 뒤의 남녀 구분 자리(1900년대 출생자는 1,2를 사용하고, 2000년대 출생자는 3,4를 사용함)를 활용하여 제한적인 확장성을 확보하고 있을 뿐 입니다.
2️⃣ 확장성
시스템은 시간이 흐르면서 이용자가 증가하는 보편화 과정이 진행되므로 자연스럽게 규모가 확장됩니다.
따라서 사용하는 구분자의 양도 증가합니다.
시스템의 최대 수용 규모를 예측하여 구분자의 최대 한계를 올바르게 설정하지 않으면, 표현할 수 있는 공간의 크기가 제한되어 시스템의 확장성도 제한받게 됩니다.
합리적인 기준을 설정하여 확장의 정도를 예측하고, 또한 그 이후에 대한 고려도 함께 이루어져야 합니다.
처음 인터넷을 설계했을 때 지금과 같은 규모로 인터넷을 이용하리라고는 예측하지 못했습니다.
그 결과 인터넷 구분자인 IP 주소의 고갈 문제에 직면해 있습니다.
3️⃣ 편리성
시스템 설계 과정에서 부여되는 구분자는 시스템의 내부 처리 구조를 효율적으로 운용할 수 있도록 해주어야 합니다.
컴퓨터 시스템은 내부적으로 숫자에 기반해 처리되기 때문에 구분자의 체계도 숫자 위주입니다.
또한 배치, 검색 등을 원활하게 수행하기 위해 보통 일반인이 의미를 이해할 수 없는 형식을 갖습니다.
이처럼 시스템 내부 동작에 종속된 구분자의 주소 체계는 사용자가 쉽게 이해하기 어려우므로 문자로 된 이름을 추가로 부여합니다.
따라서 숫자로 된 주소와 문자로 된 이름을 모두 가지므로 이를 매핑(Mapping)하는 기능이 필요합니다.
4️⃣ 정보의 함축
구분자는 응용 환경에 필요한 다양한 정보를 포함하는 경우가 많습니다.
예를 들어, 주민 번호는 생년월일, 성별 등을 알 수 있는 숫자로 구성되어 있습니다.
집 주소도 광역시부터 시작해 지역을 소규모로 분할하는 구조로 되어 있어 집의 지리적인 위치를 쉽게 가늠할 수 있습니다.
이처럼 분자는 응용 환경에 적절히 대응할 수 있는 부가 정보를 포함해야 합니다.
-
☕️[Java] 불변 객체 - 예제
☕️[Java] 불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
1️⃣ Address, ImmutableAddress 코드.
// Address
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
// ImmutableAddress
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
2️⃣ 변경 클래스 사용.
// MemberV1
public class MemberV1 {
private String name;
private Address address;
public MemberV1(String name, Address address) {
this.name = name;
this.address = address;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
// MemberMainV1
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원A 와 회원B 는 둘 다 서울에 살고 있습니다.
중간에 회원B 의 주소를 부산으로 변경해야 합니다.
그런데 회원A 와 회원B 는 Address 인스턴스를 참조하고 있습니다.
회원B 의 주소를 부산으로 변경하는 순간 회원A 의 주소도 부산으로 변경됩니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
3️⃣ 불변 클래스 사용.
// MemberV2
public class MemberV2 {
private String name;
private ImmutableAddress address;
public MemberV2(String name, ImmutableAddress address) {
this.name = name;
this.address = address;
}
public ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
MemberV2 는 주소를 변경할 수 없는, 불변인 ImmutableAddress 를 사용합니다.
// MemberMainV2
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
// memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원B 의 주소를 중간에 부산으로 변경하려고 시도합니다.
하지만 ImmutableAddress 에는 값을 변경할 수 있는 메서드가 없습니다.
따라서 컴파일 오류가 발생합니다.
결국 memberB.setAddress(new ImmutableAddress("부산")) 와 같이 새로운 주소 객체를 만들어서 전달합니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
사이드 이펙트가 발생하지 않습니다. 회원A 는 기존 주소를 그대로 유지합니다.
-
💾 [CS] 팩토리 패턴(factory pattern)
💾 [CS] 팩토리 패턴(factory pattern).
1️⃣ 팩토리 패턴(factory pattern).
팩토리 패턴(factory pattern)은 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴입니다.
상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 갖게 됩니다.
그리고 객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되니 유지 보수성이 증가됩니다.
예를 들어 라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하면 됩니다.
2️⃣ 자바의 팩토리 패턴
enum CoffeeType {
LATTE,
ESPRESSO
}
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
class Latte extends Coffee {
public Latte() {
name = "latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee type: " + type);
}
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // latte
}
}
3️⃣ 코드 설명.
팩토리 패턴(Factory Pattern) 은 객체 생성의 로직을 별도의 클래스나 메서드로 분리하여 관리하는 디자인 패턴입니다.
이는 객체 생성에 관련된 코드를 클라이언트 코드에서 분리하여, 객체 생성의 변화에 대한 유연성을 높이고 코드의 유지보수성을 개선하는 데 도움이 됩니다.
팩토리 패턴 은 크게 팩토리 메서드 패턴 과 추상 팩토리 패턴 으로 구분되며, 위 코드 예시는 팩토리 메스드 패턴 의 전형적인 예입니다.
1. CoffeeType 열거형(Enum)
enum CoffeeType {
LATTE,
ESPRESSO
}
설명 : CoffeeType 은 커피의 종류를 나타내는 열거형(Enum)입니다.
이 열거형은 LATTE 와 ESPRESSO 두 가지 타입의 커피를 정의하고 있습니다.
역할 : 커피의 종류를 코드 내에서 명확하게 구분하고, CoffeeFactory 에서 커피 객체를 생성할 때 사용됩니다.
2. Coffee 추상 클래스.
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
설명 : Coffee 는 커피 객체의 공통된 속성과 메서드를 정의한 추상 클래스입니다.
name 필드는 커피의 이름을 저장하며, getName() 메서드는 커피의 이름을 반환합니다.
역할 : 구체적인 커피 클래스들이 상속받아야 하는 공통적인 기능을 정의합니다.
3. Latte 와 Espresso 클래스.
class Latte extends Coffee {
public Latte() {
name = "latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
설명 : Latte 와 Espresso 는 Coffee 클래스를 상속받아 구체적인 커피 타입을 구현한 클래스들입니다.
각 클래스는 생성자에서 name 필드를 특정 커피 이름으로 초기화합니다.
역할 : 특정 커피 타입의 객체를 생성하는 역할을 합니다.
4. CoffeeFactory 클래스.
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee tyep: " + type);
}
}
}
설명 : CoffeeFactory 클래스는 팩토리 패턴의 핵심으로, createCoffee() 메서드를 통해 특정 타입의 커피 객체를 생성하여 반환합니다.
CoffeeType 열거형에 따라 적절한 커피 객체를 생성합니다.
역할 : 객체 생성의 로직을 중앙 집중화하여 클라이언트 코드에서 객체 생성의 책임을 분리합니다.
클라이언트는 CoffeeFactory 의 createCoffee() 메서드를 호출하여 원하는 커피 객체를 생성할 수 있습니다.
5. Main 클래스.
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // latte
}
}
설명 : Main 클래스는 클라이언트 코드로, CoffeeFactory 를 사용하여 LATTE 타입의 커피 객체를 생성하고, 그 이름을 출력합니다.
역할 : 팩토리 패턴을 사용하는 클라이언트 코드로, 직접적으로 객체를 생성하지 않고 팩토리를 통해 객체를 생성합니다.
4️⃣ 팩토리 패턴의 장점.
1. 코드의 유연성 증가.
객체 생성 로직이 중앙화되어 있으므로, 새로운 커피 타입을 추가할 때 클라이언트 코드를 수정할 필요 없이 팩토리 클래스만 수정하면 됩니다.
2. 유지보수성 향상.
객체 생성 코드가 한 곳에 모여 있어 코드의 유지보수가 쉬워집니다.
객체 생성 과정에서의 변경이 필요한 경우에도 팩토리 클래스만 수정하면 됩니다.
3. 코드의 결합도 감소.
클라이언트 코드는 구체적인 클래스에 의존하지 않고, 인터페이스나 추상 클래스를 통해 객체를 다루기 때문에 결합도가 낮아집니다.
5️⃣ 팩토리 패턴의 단점.
1. 클래스의 복잡성 증가.
객체 생성을 위한 팩토리 클래스가 추가됨으로써 클래스의 수가 증가하고, 코드 구조가 다소 복잡해질 수 있습니다.
2. 확장 시 주의 필요.
새로운 커피 타입을 추가할 때마다 팩토리 클래스의 switch 문이나 if-else 문이 증가할 수 있어, 확장성이 제한될 수 있습니다.
이 문제를 해결하기 위해서는 추상 팩토리 패턴이나 다른 디자인 패턴과 결합하는 방법을 고려할 수 있습니다.
6️⃣ 결론.
팩토리 패턴은 객체 생성의 책임을 분리하여 코드의 유연성과 유지보수성을 높이는 강력한 디자인 패턴입니다.
위 코드 예시에서는 커피 객체를 생성하는 로직을 CoffeeFactory 클래스에 모아두어, 클라이언트 코드가 특정 커피 클래스에 직접적으로 의존하지 않도록 하였습니다.
이를 통해 클라이언트 코드는 커피 객체의 생성 방식에 대해 신경 쓰지 않고도 다양한 타입의 커피를 생성하고 사용할 수 있게 됩니다.
-
💾 [CS] 추상화(Abstraction)
💾 [CS] 추상화(Abstraction).
1️⃣ 추상화(Abstraction).
추상화(Abstraction) 는 객체 지향 프로그래밍(Object-Oriented-Programming, OOP)의 중요한 개념 중 하나로, 복잡한 시스템에서 핵심적인 개념이나 기능만을 추려내어 단순화하는 과정입니다.
이를 통해 불필요한 세부 사항을 감추고, 중요한 속성이나 행위만을 노출하여 시스템을 보다 간단하게 이해하고 사용할 수 있게 합니다.
1️⃣ 추상화의 핵심 개념.
1. 본질적인 것만 노출.
시스템의 복잡한 내부 구현을 숨기고, 외부에서는 중요한 기능이나 속성만을 사용할 수 있도록 설계합니다.
예를 들어, 자동차를 운전할 때 운전자는 엔진의 작동 원리나 내부 구조를 몰라도, 운전대, 가속 페달, 브레이크 등의 중요한 인터페이스를 통해 자동차를 조작할 수 있습니다.
2. 복잡성 감소.
추상화를 통해 사용자에게 복잡한 시스템을 단순하게 보이도록 하여, 사용자가 시스템을 쉽게 이해하고 사용할 수 있게 합니다.
이는 특히 큰 시스템이나 라이브러리를 설계할 때 중요합니다.
3. 재사용성과 유지보수성 향상.
추상화를 사용하면, 코드의 재사용성을 높이고 유지보수성을 향상시킬 수 있습니다.
동일한 추상 인터페이스를 구현하는 여러 클래스가 있을 때, 구체적인 클래스 구현을 신경 쓰지 않고 인터페이스를 통해 일관된 방식으로 코드를 사용할 수 있습니다.
2️⃣ 추상화의 예
추상화는 주로 추상 클래스와 인터페이스 를 통해 구현됩니다.
추상 클래스.
추상 클래스는 하나 이상의 추상 메서드를 초함하는 클래스입니다.
추상 메서드는 선언만 되어 있고, 구체적인 구현은 해당 클래스를 상속받는 하위 클래스에서 제공해야 합니다.
abstract class Animal {
abstract void sound(); // 추상 메서드
void breathe() { // 구체적인 메서드
System.out.println("Breathing");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
이 예에서 Animal 클래스는 추상 클래스이고, sound() 메서드는 추상 메서드입니다.
Dog 와 Cat 클래스는 sound() 메서드를 구체적으로 구현합니다.
Animal 클래스는 동물의 일반적인 특징인 breath() 메서드를 포함하지만, sound() 는 동물마다 다르므로 하위 클래스에서 구체화됩니다.
인터페이스
인터페이스는 추상화의 또 다른 형태로, 클래스가 구현해야 하는 메서드의 선언을 포함합니다.
인터페이스 자체는 구현을 가지지 않으며, 구현은 이를 구현하는 클래스에서 제공됩니다.
interface Flyable {
void fly(); // 추상 메서드
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("Airplane is flying");
}
}
여기서 Flyable 인터페이스는 fly() 라는 추상 메서드를 선언하고 있으며, Bird 와 Airplane 클래스는 각각 이 메서드를 구현합니다.
Flyable 인터페이스를 통해, 비행할 수 있는 객체들은 동일한 방식으로 취급될 수 있습니다.
3️⃣ 추상화의 장점.
1. 코드의 간결성.
중요한 부분만 남기고 복잡한 구현 세부 사항을 숨겨, 코드를 간결하고 이해하기 쉽게 만듭니다.
2. 유연한 설계.
구체적인 구현에 의존하지 않기 때문에, 다양한 구현체를 쉽게 교체하거나 확장할 수 있습니다.
3. 재사용성 증가.
추상 클래스나 인터페이스를 통해 여러 클래스에서 공통적으로 사용될 수 있는 구조를 만들 수 있습니다.
4️⃣ 요약.
추상화는 복잡한 시스템에서 불필요한 세부 사항을 감추고 중요한 부분만을 노출하여 시스템을 간단하게 만드는 개념입니다.
이를 통해 코드의 복잡성을 줄이고, 유연성과 재사용성을 높이며, 유지보수를 용이하게 할 수 있습니다.
추상화는 주로 추상 클래스와 인터페이스를 통해 구현됩니다.
-
☕️[Java] 테스트 코드와 Reflection.
☕️[Java] 테스트 코드와 Reflection.
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
}
2️⃣ MemberService를 Test.
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void setUp() throws Exception {
memberService = new MemberService();
memberRepository = new MemoryMemberRepository();
// Reflection을 사용하여 memberRepository 필드에 값을 설정.
Field repositoryField = MemberService.class.getDeclaredField("memberRepository");
repositoryField.setAccessible(true);
repositoryField.set(memberService, memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long savedId = memberService.join(member);
// then
Optional<Member> foundMember = memberRepository.findById(savedId);
assertThat(foundMember.isPresent()).isTrue();
assertThat(foundMember.get().getName()).isEqualTo("spring");
}
@Test
public void 중복_회원_제외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// join
memberService.join(member1);
// then
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
memberService.join(member2);
});
assertThat(exception.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
public void 전체회원조회() {
// given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring2");
memberService.join(member1);
memberService.join(member2);
// when
List<Member> members = memberService.findMembers();
// then
assertThat(members.size()).isEqualTo(2);
assertThat(members).contains(member1, member2);
}
}
3️⃣ 모르는 코드 설명.
"@BeforeEach" 애노테이션.
JUnit 5에서 테스트 메서드가 실행되기 전에 매번 호출되는 메서드에 사용됩니다.
이 애노테이션이 붙은 메서드는 각 테스트 메서드가 실행되기 직전에 실행되므로, 테스트 환경을 초기화하거나 준비 작업을 수행하는 데 유용합니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
테스트 환경 초기화
각 테스트 메서드가 실행될 때마다 동일한 초기 상태를 보장하기 위해 사용됩니다.
예를 들어, 테스트할 객체를 새로 생성하거나, 필요한 데이터를 설정하는 등의 작업을 수행합니다.
반복 작업 처리
여러 테스트에서 반복적으로 수행해야 하는 설정 작업이 있을 때, "@BeforeEach" 를 사용하여 중복 코드를 줄일 수 있습니다.
독립적인 테스트 보장
테스트 간의 상호 의존성을 없애고, 각 테스트가 독립적으로 실행되도록 보장할 수 있습니다.
이를 통해 테스트 간에 상태가 공유되지 않도록 하여 신뢰성 있는 테스트를 구현할 수 있습니다.
"@BeforeEach" 는 테스트 환경을 일관되게 유지하고,
"@AfterEach" 애노테이션.
JUnit 5에서 각 테스트 매서드가 실행된 후에 실행되는 메서드를 붙이는 애노테이션 입니다.
이 메서드는 테스트가 완료된 후에 정리(clean-up) 작업을 수행하는 데 사용됩니다.
🙋♂️ 이 애노테이션의 주요 역할은 다음과 같습니다.
자원 정리
테스트 중에 사용된 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하거나 정리하는 데 사용됩니다.
이는 메모리 누수나 리소스 잠금을 방지할 수 있습니다.
테스트 환경 복원
테스트 실행 중에 변경된 상태나 데이터를 초기 상태로 되돌려, 다른 테스트에 영향을 미치지 않도록 합니다.
이는 테스트 간의 독립성을 유지하는 데 중요한 역할을 합니다.
로그 남기기
테스트가 끝난 후 테스트 결과나 상태에 대한 로그를 기록할 수 있습니다.
이를 통해 테스트 결과를 모니터링하거나 디버깅할 때 유용할 수 있습니다.
"@AfterEach" 는 테스트 후에 정리 작업을 자동으로 수행하여, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
Reflection
Reflection 은 자바에서 런타임 시에 클래스, 인터페이스, 메서드, 필드 등의 정보를 동적으로 조사하고, 조작할 수 있는 기능을 제공합니다.
Reflection 을 사용하면 코드에서 특정 객체의 클래스 타입이나 메서드, 필드 등에 접근하고, 해당 요소들을 동적으로 호출하거나 값을 변경하는 것이 가능합니다.
🙋♂️ Reflection의 주요 개념과 역할은 다음과 같습니다.
클래스 정보 조사
Reflection을 사용하면 특정 객체의 클래스 타입을 런타임에 알아낼 수 있습니다.
예를 들어, Class<?> clazz = obj.getClass(); 를 사용하여 객체 obj 의 클래스 정보를 가져올 수 있습니다.
필드, 메서드, 생성자 접근
Reflection을 통해 클래스에 선언된 필드, 메서드 ,생성자에 접근할 수 있습니다.
이를 통해 특정 필드의 값을 가져오거나 설정하고, 메서드를 호출하거나 생성자를 통해 객체를 생성할 수 있습니다.
예를 들어, Field field = clazz.getDeclaredField("fieldName);" 를 사용하여 특정 필드에 접근할 수 있습니다.
접근 제어 무시
Reflection을 사용하면 private 으로 선언된 필드나 메서드에도 접근할 수 있습니다. setAccessible(true) 메서드를 사용하여 접근 제어자를 무시할 수 있습니다.
이는 보통 테스트나 프레임워크에서 사용되며 예를 들어, 프레임워크에서 자동으로 의존성을 주입하거나, 테스트에서 private 필드에 접근할 때 유용합니다.
런타임에 동적 객체 생성 및 메서드 호출
Reflection을 사용하여 런타임에 동적으로 객체를 생성하거나 메서드를 호출할 수 있습니다.
이는 매우 유연한 코드 작성을 가능하게 하지만, 일반적으로 성능 저하가 있을 수 있습니다.
애노테이션 처리
Reflection을 사용하여 클래스나 메서드에 선언된 애노테이션을 런타임에 읽어들이고 처리할 수 있습니다.
이는 주로 프레임워크에서 사용되며, 예를 들어, 스프링 프레임워크에서 애노테이션 기반으로 설정을 처리하는 경우가 있습니다.
🙋♂️ Reflection의 장단점은 다음과 같습니다.
장점
동적 기능 제공 : 코드의 유연성과 확장성을 높여줍니다. 런타임에 클래스나 메서드를 동적으로 호출할 수 있어, 컴파일 타임에 알 수 없는 구조를 처리할 수 있습니다.
프레임워크에서 유용 : 많은 자바 프레임워크, 예를 들어 스프링(Spring), 하이버네이트(Hibernate) 등은 Reflection을 활용하여 애플리케이션의 동작을 제어합니다.
단점
성능 이슈 : Reflection은 일반적인 메서드 호출에 비해 성능이 떨어질 수 있습니다. 따라서 중요한 애플리케이션에서는 중의가 필요합니다.
안전성 문제 : Reflection을 사용하면 컴파일 타임에 확인할 수 없는 동작이 많아, 잘못 사용하면 런타임 에러가 발생할 수 있습니다.
보안 이슈 : Reflection은 접근 제어를 무시할 수 있으므로, 잘못된 사용은 보안상의 취약점을 초래할 수 있습니다.
4️⃣ 코드 설명.
Reflection을 사용하여 memberRepository 설정.
MemberService 의 memberRepository 필드는 private final 로 선언되어 있으므로, 일반적인 방식으로 접근할 수 없습니다.
이 문제를 해결하기 위해 Reflection 을 사용하여 필드에 접근하고, 테스트용 MemoryMemberRepository 를 설정합니다.
@BeforeEach 애노테이션.
각 테스트가 실행되기 전에 MemberService 인스턴스를 생성하고. Reflection 을 사용하여 memberRepository 필드를 MemoryMemberRepository 인스턴스로 초기화합니다.
@AfterEach 애노테이션.
각 테스트가 완료된 후, MemoryMemberRepository 의 저장소를 초기화하여 테스트 간의 상태 간섭을 방지합니다.
테스트 메서드.
회원가입 : 새로운 회원을 가입시키고, 저장된 회원이 올바르게 반환되는지 검증합니다.
중복_회원_제외 : 중복된 이름으로 회원을 가입하려 할 때 IllegalStateException 이 발생하는지를 확인합니다.
전체회원조회 : 저장된 모든 회원이 올바르게 반환되는지 검증합니다.
이 방법을 통해 MemberService 를 수정하지 않고도 해당 클래스의 동작을 테스트할 수 있습니다.
다만, 실제 코드에서는 생성자를 통한 주입이나 다른 테스트 가능한 구조로 변경하는 것이 더 바람직합니다.
-
☕️[Java] 테스트 코드와 Dependancy Injection
☕️[Java] 테스트 코드와 Dependancy Injection
1️⃣ 전체 코드.
// Member
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// MemberRepository - Interface
import com.devkobe.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// MemoryMemberRepository
import com.devkobe.hello_spring.domain.Member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
// MemberService
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemberRepository;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/*
* 회원 가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
// MemberServiceTest
import com.devkobe.hello_spring.domain.Member;
import com.devkobe.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
void findMembers() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
@Test
void findOne() {
}
}
2️⃣ 의존성 주입(DI, Dependency Injection).
의존성 주입(DI, Dependency Injection) 는 객체 지향 프로그래밍에서 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다.
1️⃣ 의존성 주입(DI, Dependency Injection)의 개념.
의존성(Dependency)
클래스가 다른 클래스의 기능을 사용해야 하는 상황을 의미합니다.
예를 들어, MemberService 클래스는 회원 데이터를 처리하기 위해 MemberRepository 를 필요로 합니다. 여기서 MemberService 는 MemberRepository 에 의존합니다.
주입(Injection)
외부에서 객체를 생성하여 주입해주는 것을 의미합니다.
이는 보통 생성자 주입, 세터(Setter) 주입, 필드 주입 등의 방식으로 이루어집니다.
2️⃣ DI의 주요 목적.
느슨한 결합(Loose Coupling)
DI를 통해 클래스 간의 결합도를 낮출 수 있습니다.
클래스는 자신이 사용하는 의존 객체가 무엇인지 알 필요가 없고, 이로 인해 클래스 간의 결합이 느슨해집니다.
이는 코드의 유연성을 높여주며, 코드 변셩 시 영향 범위를 줄여줍니다.
유연성 및 확장성
의존 객체를 외부에서 주입받기 때문에, 애플리케이션이 다른 의존 객체로 쉽게 전환될 수 있습니다.
예를 들어, 테스트 환경에서 MemoryMemberRepository 를 사용하고, 실제 운영 환경에서는 데이터베이스 연결을 사용하는 JpaMemberRepository 를 사용할 수 있습니다.
테스트 용이성
DI를 통해 클래스 간의 의존 관계를 외부에서 주입받으면, 테스트 시에 쉽게 Mock 객체나 Stub 객체를 주입할 수 있어 테스트를 더 용이하게 만듭니다.
3️⃣ DI의 사용 방법.
1. 생성자 주입(Constructor Injection) : 의존성을 클래스의 생성자를 통해 주입하는 방법입니다. 위 코드에서는 생성자 주입이 사용되었습니다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
2. 세터 주입(Setter Injection) : 의존성을 세터 메서드를 통해 주입하는 방법입니다.
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
3. 필드 주입(Field Injection) : 의존성을 필드에 직접 주입하는 방법으로, 보통 @Autowired 와 같은 애노테이션을 사용합니다. 하지만 필드 주입은 테스트하기 어려울 수 있어 일반적으로 권장되지 않습니다.
DI는 코드의 재사용성, 유지보수성, 테스트 용이성을 높여주는 중요한 디자인 패턴으로, 스프링 프레임워크와 같은 의존성 주입 컨테이너에서 널리 사용됩니다.
3️⃣ 코드에서 의존성 주입(DI, Dependency Injection)이 사용된 부분과 설명.
위 코드에서 DI(Dependency Injection)가 사용된 부분은 MemberService 클래스의 인스턴스를 생성하는 부분입니다.
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
이 부분에서 MemberService 클래스의 생성자에 MemoryMemberRepository 객체를 주입하고 있습니다.
MemberService 클래스는 생성자에서 MemberRepository 인터페이스를 받아 사용합니다.
아렇게 함으로써, MemberService 는 직접적으로 MemoryMemberRepository 를 생성하지 않고 외부에서 주입받게 됩니다.
1️⃣ 설명.
memberRepository = new MemoryMemberRepository();
MemoryMemberRepository 객체를 생성합니다. 이는 MemberRepository 인터페이스의 구현체입니다.
memberService = new MemberService(memberRepository);
MemberService 객체를 생성 할 때, 생성자에 memberRepository 를 주입합니다.
이 방식은 MemberService 가 MemoryMemberRepository 에 강하게 결합되지 않도록 하여, 다른 구현체로 쉽게 변경할 수 있게 만듭니다.
예를 들어, 테스트 환경에서는 MemoryMemberRepository 를 사용하고, 실제 서비스에서는 데이터베이스와 연결된 구현체를 사용할 수 있습니다.
-
-
💾 [CS] 의존성 주입(DI, Dependency Injection)
💾 [CS] 의존성 주입(DI, Dependency Injection).
1️⃣ 의존성 주입(DI, Dependency Injection)
싱글톤 패턴과 같이 사용하기 쉽고 굉장히 실용적이지만 모듈 간의 결합을 강하게 만들 수 있는 단점이 있는 패턴의 경우 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있습니다.
의존성이란 종속성이라고도 하며 A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A 또한 변해야 된다는 것을 의미합니다.
앞의 그림처럼 메인 모듈(main module)이 ‘직접’ 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(dependency injector)가 이 부분을 가로채 메인 모듈이 "간접적" 으로 의존성을 주입하는 방식입니다.
이를 통해 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 됩니다.
참고로 이를 ‘디커플링이 된다’ 고도 합니다.
1️⃣ 의존성 주입의 장점
모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션하기도 수월합니다.
또한, 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션 의존성 방향이 일관되고, 애플리케이션을 쉽게 추론할 수 있으며, 모듈 간의 관계들이 조금 더 명확해집니다.
2️⃣ 의존성 주입의 단점
모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며 약간의 런타임 페널티가 생기기도 합니다.
3️⃣ 의존성 주입 원칙
의존성 주입은 "상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 합니다. 또한, 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 합니다." 라는 의존성 주입 원칙을 지켜주면서 만들어야 합니다.
위 문장에서 “추상화”의 의미.
문장에서 “추상화”는 구체적인 구현에 의존하지 않고, 일반화된 인터페이스나 추상 클래스 등에 의존해야 한다는 것을 뜻합니다.
이 원칙은 의존성 역전 원칙(DIP, Dependency Inversion Principle) 과 관련이 있습니다.
상위 모듈 : 애플리케이션의 상위 계층에서 동작하는 코드, 즉 더 높은 수준의 정책이나 로직을 구현하는 모듈입니다.
하위 모듈 : 상위 모듈에서 호출하거나 사용하는 구체적인 기능이나 세부 사항을 구현하는 코드입니다.
1️⃣ 추상화.
“추상화” 는 객채 지향 프로그래밍(OOP)애서 중요한 개념 중 하나로, 구체적인 구현(details)을 감추고, 더 높은 수준의 개념을 정의하는 것을 의미합니다.
추상화는 구체적인 것보다는 더 일반적이고 보편적인 개념을 다루며, 특정한 구현 사항에 의존하지 않고 인터페이스나 추상 클래스 등을 통해 기능을 정의합니다.
2️⃣ 의존성 주입과 추상화의 관계.
의존성 주입(DI, Dependency Injection)은 의존성 역전 원칙(DIP, Dependency Inversion Principle)을 구현하기 위한 방법 중 하나입니다.
의존성 주입을 사용하면, 상위 모듈이 하위 모듈의 구체적인 구현에 의존하지 않고, 하위 모듈이 구현한 추상화(인터페이스나 추상 클래스)에 의존하도록 코드를 설계할 수 있습니다.
즉, 상위 모듈과 하위 모듈 모두 추상화된 인터페이스에 의존하게 하여, 구체적인 구현이 변경되더라도 상위 모듈의 코드가 영향을 받지 않도록 합니다.
3️⃣ 예시.
아래는 추상화와 의존성 주입을 적용한 예시입니다.
// 추상화된 인터페이스 (추상화)
public interface PaymentProcessor {
void processPayment(double amount);
}
// 하위 모듈 - 구체적인 구현
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// PayPal을 통해 결제 처리
}
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 신용카드를 통해 결제 처리
}
}
// 상위 모듈 - 추상화에 의존함
public class PaymentService {
private PaymentProcessor paymentProcessor;
// 의존성 주입을 통해 구현체를 주입 받음
public PaymentService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void makePayment(double amount) {
paymentProcessor.processPayment(amount);
}
}
위 코드에서 "PaymentService" 는 "PaymentProcessor" 라는 추상화에 의존합니다.
"PaymentService" 는 "PayPalProcessor" 나 "CreditCardProcessor" 의 구체적인 구현을 알 필요가 없으며, 단지 "PaymentProcessor" 인터페이스에 정의된 메서드를 호출합니다.
- 이를 통해 결제 처리 방식이 PayPal에서 신용카드로 변경되더라도 "PaymentService" 는 수정할 필요가 없습나다.
이처럼 “추상화”는 상위 모듈과 하위 모듈이 특정 구현이 아닌, 일반적인 개념에 의존하도록 만들어줌으로써, 코드의 유연성과 재사용성을 높여주는 중요한 개념입니다.
-
-
☕️[Java] 공유 참조와 사이드 이펙트
☕️[Java] 공유 참조와 사이드 이펙트
1️⃣ 공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 "b" 의 값을 부산으로 변경한 코드를 다시 분석해봅시다.
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 "b" 의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 "a" , "b" 는 같은 인스턴스를 참조합니다.
따라서 "a" 의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트(Side Effect)라 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그래밍의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
2️⃣ 사이드 이펙트 해결 방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 "a" 와 "b" 가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
public class RefMain1_2 {
public static void main(String[] args) {
Address a = new Address("서울"); // x001
Address b = new Address("서울"); // x002
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b);
}
}
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 "b" 의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
그림 - 생성 코드
"a" 와 "b" 는 서로 다른 "Address" 인스턴스를 참조합니다.
그림 - 변경 코드
"a" 와 "b" 는 서로 다른 인스턴스를 참조합니다.
따라서 "b" 가 참조하는 인스턴스의 값을 변경해도 "a" 에는 영향을 주지 않습니다.
3️⃣ 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 "a" , "b" 가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 "a", "b" 가 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유.
Address a = new Address("서울");
Address b = a;
이 경우 "a", "b" 둘 다 같은 "Address" 인스턴스를 바라보기 때문에 한 쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유하지 않음.
Address a = new Address("서울");
Address ㅠ = new Address("서울");
이 경우 "a", "b" 는 서로 다른 "Address" 인스턴스를 바라보기 때문에 한 쪽의 주소만 부산으로 변경하는 것이 가능합니다.
이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다.
즉, 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다.
그런데 여기서 문제가 있습니다.
하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다.
아래의 예시를 봐봅시다.
참조값의 공유를 막을 수 있는 방법이 없다.
Address a = new Address("서울");
Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다.
"b = a" 와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다.
하지만 "Address" 를 사용하는 개발자 입장에서 실수로 "b = a" 라고 해도 아무런 오류가 발생하지 않습니다.
왜냐하면 자바 문법상 "Address b = a" 와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다.
Address b = new Address("서울"); // 새로운 객체 참조
Address b = a; // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
즉, 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 신경써서 코드를 작성한다면 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
```java
public class RefMain1_3 {
public static void main(String[] args) {
Address a = new Address(“서울”);
Address b = a;
System.out.println(“a = “ + a);
System.out.println(“b = “ + b);
change(b,"부산");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b); }
private static void change(Address address, String changeAddress) {
System.out.println(“주소 값을 변경합니다 -> “ + changeAddress);
address.setValue(changeAddress);
}
}
```
앞서 작성한 코드와 같은 코드입니다.
단순히 "change()" 메서드만 하나 추가되었습니다.
그리고 "change()" 메서드에서 "Address" 인스턴스에 있는 "value" 값을 변경합니다.
"main()" 메서드만 보면 "a" 의 값이 함께 부산으로 변경된 이유를 찾기가 더 어렵습니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}
-
☕️[Java] 불변 객체 - 도입
☕️[Java] 불변 객체 - 도입.
1️⃣ 불변 객체 - 도입.
공유하면 안되는 객체를 여러 변수에서 공유하기 때문에 문제가 발생했었습니다.
그렇다고 객체의 공유를 막을 수 있는 방법은 없습니다.
그러나 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
"a", "b" 는 처음 시점에는 둘 다 "서울" 이라는 주소를 사용해야 합니다.
그리고 이후에 "b" 의 주소를 "부산" 으로 변경해야 합니다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 "b = a" 와 같이 "서울" 이라는 "Address" 인스턴스를 "a", "b" 가 함께 사용하는 것이, 다음 코드와 같이 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다.
인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 "Address b = a" 와 같이 공유 참조를 사용해도 아무런 문제가 없습니다.
오히려 더 효율적입니다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다.
b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다.
기본형과 다르게 참조형인 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다.
따라서 이것은 문제가 아닙니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 어디선가 변경했기 때문입니다.
만약 "Address" 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
2️⃣ 불변 객체 도입.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
"Address" 클래스를 상태가 변하지 않는 불변 클래스로 만들어 보겠습니다.
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
내부 값이 변경되면 안됩니다.
따라서 "value" 의 필드를 "final" 로 선언했습니다.
값을 변경할 수 있는 "setValue()" 를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
```java
public class RefMain2 {
public static void main(String[] args) {
ImmutableAddress a = new ImmutableAddress(“서울”);
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다.
System.out.println(“a = “ + a);
System.out.println(“b = “ + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이팩트 발생
System.out.println("b = " + b); } } ```
"ImmutableAddress" 의 경우 값을 변경할 수 있는 "b.setValue()" 메서드 자체가 제거되었습니다.
이제 "ImmutableAddress" 인스턴스의 값을 변경할 수 있는 방법은 없습니다.
"ImmutableAddress" 를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫게 됩니다.
예를 들어 "b.setValue("부산")" 을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지합니다.
따라서 어쩔 수 없이 새로운 "ImmutableAddress("부산")" 인스턴스를 생성해서 "b" 에 대입합니다.
결과적으로 "a", "b" 는 서로 다른 인스턴스를 참조하고, "a" 가 참조하던 "ImmutableAddress" 는 그대로 유지됩니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 "a" 의 값은 그대로 유지되는 것을 확인할 수 있습니다.
자바에서 객체의 공유 참조는 막을 수 없습니다.
"ImmutableAddress" 는 불변 객체입니다.
따라서 값을 변경할 수 없습니다.
"ImmutableAddress" 은 불변 객체이므로 "b" 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
3️⃣ 정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
🙋♂️ 참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.
가변은 사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.
불변은 사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.
"Address" 는 가변 클래스입니다.
이 클래스로 객체를 생성하면 가변 객체가 됩니다.
"ImmutableAddress" 는 불변 클래스입니다.
이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
-
-
-
☕️[Java] Object와 OCP - 2
☕️[Java] Object와 OCP - 2.
1️⃣ Object.
Java에서 'Object' 는 모든 클래스의 최상위 부모 클래스이자, Java 클래스 계층 구조의 최상위에 위치하는 클래스입니다.
Java에서 모든 클래스는 암묵적으로 'Object' 클래스를 상속받으며, 이로 인해 'Object' 클래스가 제공하는 메서드를 사용할 수 있습니다.
이는 Java의 객체 지향 프로그래밍(OOP)에서 중요한 역할을 합니다.
1️⃣ Object 클래스의 주요 역할.
1. 최상위 클래스.
모든 Java 클래스는 'Objcet' 클래스를 상속받기 때문에 'Object' 클래스에서 제공하는 메서드는 모든 객체에서 사용할 수 있습니다.
2. 기본 메서드 제공.
'Object' 클래스는 모든 객체가 기본적으로 사용할 수 있는 몇 가지 중요한 메서드를 제공합니다.
예를 들어 다음과 같습니다.
'equals(Object obj)'
두 객체가 같은지를 비교합니다.
'hashCode()'
객체의 해시 코드를 반환합니다. 이 값은 객체를 식별하는 데 사용됩니다.
'toString()'
객체를 문자열로 표현합니다.
'clone()'
객체를 복제합니다.(단, 클래스에서 'Cloneable' 인터페이스를 구현해야 사용 가능)
'finalize()'
객체가 가비지 컬렉션되기 전에 호출됩니다.
3. 다형성 지원.
'Object' 타입으로 모든 객체를 참조할 수 있으므로, 다양한 객체를 처리하는 메서드나 컬렉션에서 유연성을 제공합니다.
예를 들어, Java의 컬렉션 프레임워크에서는 'Object' 타입을 사용하여 다양한 타입의 객체를 저장할 수 있습니다.
4. 공통 기능의 확장.
모든 클래스가 'Object' 를 상속받기 때문에, 'Object' 클래스의 메서드를 재정의(Override)하여 클래스에 맞는 동작을 구현할 수 있습니다.
예를 들어, 'toString()' 메서드를 재정의하여 객체의 상태를 의미 있는 문자열로 표현할 수 있습니다.
2️⃣ 예시.
다음은 'Object' 클래스의 메서드를 활용하는 간단한 예시입니다.
public class Person {
private String name;
private 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 String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
System.out.println(person1.equals(person2)); // true
System.out.println(person1.toString()); // Person{name='Alice', age=30}
}
}
이 예시에서 'equals' 와 'toString' 메서드는 'Object' 클래스에서 제공하는 메서드를 재정의하여 'Person' 클래스의 객체에 적합한 동작을 정의하고 있습니다.
3️⃣ 결론.
Java의 'Object' 클래스는 모든 클래스의 공통 조상으로서 중요한 역할을 하며, 기본적인 객체 비교, 해시 코드 생성, 문자열 표현 등의 기능을 제공합니다.
이는 Java에서의 객체 지향 프로그래밍의 기초를 이루며, 다양한 클래스 간의 상호작용을 가능하게 합니다.
2️⃣ OCP
OCP는 “Open/Closed Principle”의 약자로, 객체 지향 설계의 중요한 원칙 중 하나입니다.
이 원칙은 Robert C. Martin에 의해 정의된 SOLID 원칙 중 하나로, 소프트웨어 설계의 유연성과 유지보수성을 높이기 위한 지침을 제공합니다.
1️⃣ OCP(Open/Closed Principle)의 정의
“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 열려 있어야 하고, 수정에는 닫혀 있어야 한다.” 라는 원칙입니다.
확장에 열려 있어야 한다(Open for extension)
새로운 기능이나 요구사항이 추가될 때, 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다는 뜻입니다.
이를 통해 소프트웨어를 유연하게 확장할 수 있으며, 새로운 기능을 도입할 때 기존 코드에 영향을 주지 않게 됩니다.
수정에 닫혀 있어야 한다(Closed for modification)
기존에 잘 작동하던 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 뜻입니다.
이는 소프트웨어의 안정성을 유지하면서 변경의 영향을 최소화할 수 있습니다.
2️⃣ OCP의 구현 방법.
OCP를 구현하는 가장 일반적인 방법은 추상화 와 다형성 을 사용하는 것입니다.
추상 클래스나 인터페이스를 통해 기본 구조를 정의하고, 이를 상속하거나 구현하여 구체적인 기능을 확장합니다.
이렇게 하면 기존 코드베이스를 변경하지 않고도 새로운 기능을 추가할 수 있습니다.
3️⃣ 예시.
// 기존 코드: Shape 인터페이스 정의
interface Shape {
double calculateArea();
}
// 기존 코드: Rectangle 클래스 정의
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
// 확장 코드: Circle 클래스 정의 (기존 코드를 수정하지 않음)
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// 클라이언트 코드
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 20);
Shape circle = new Circle(5);
System.out.println("Rectangle Area: " + rectangle.calculateArea());
System.out.println("Circle Area: " + circle.calculateArea());
}
}
설명
이 예시에서 'Shape' 인터페이스는 면적을 계산하는 'calculateArea()' 메서드를 정의합니다.
'Rectangle' 클래스는 이 인터페이스를 구현하여 사각형의 면적을 계산하는 기능을 제공합니다.
이후, 'Circle' 클래스를 추가하면서 새로운 기능을 확장합니다.
이 과정에서 기존의 'Rectangle' 클래스나 'Shape' 인터페이스의 코드는 전혀 수정하지 않고 새로운 기능을 추가할 수 있었습니다.
이처럼 OCP를 잘 준수하면 소프트웨어가 변화하는 요구사항에 유연하게 대응할 수 있으며, 유지보수 비용을 줄이고 코드의 재사용성을 높일 수 있습니다.
3️⃣ Object와 OCP의 관계.
'Object' 와 'OCP(Open/Closed Principle)' 는 둘 다 객체 지향 프로그래밍의 개념과 밀접하게 관련되어 있지만, 그 역할과 목적은 다릅니다.
이 둘의 관계ㄴ를 이해하려면 먼저 각각의 역할을 간단히 요약하고, 그 후에 이들이 어떻게 상호작용하는지 설명할 수 있습니다.
1️⃣ ‘Object’ 클래스의 역할
Java에서 'Object' 클래스는 모든 클래스의 최상위 부모 클래스입니다.
모든 Java 클래스는 암묵적으로 'Object' 를 상속받으며, 'Object' 클래스에서 제공하는 메서드를 사용할 수 있습니다.
이 클래스는 Java에서 객체의 기본적인 기능(예: 'equalse', 'hashCode', 'toString')을 제공합니다.
'Object' 클래스 자체는 특정한 설계 원칙을 강제하지 않지만, 객체 지향 프로그래밍의 근본적인 기초를 제공합니다.
2️⃣ OCP(Open/Closed Principle)의 역할.
OCP는 소프트웨어 설계 원칙으로, 소프트웨어가 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다 는 원칙을 따릅니다.
이 원틱은 소프트웨어를 설계할 때 변화하는 요구사항에 유연하게 대응하고, 코드의 수정 없이도 기능을 확장할 수 있도록 구조화하는 방법론입니다.
이는 주로 인터페이스, 추상 클래스, 상속, 다형성 등을 통해 구현됩니다.
3️⃣ ‘Object’와 OCP의 관계.
1. 기본 클래스 구조 제공.
‘Object’ 클래스는 모든 클래스의 기본이 되며, OCP 원칙을 적용하기 위한 기본 구조를 제공합니다.
예를 들어, 모든 클래스가 'Object' 를 상속받기 때문에, 클래스 설계자는 'Object' 클래스에서 제공하는 메서드(예: 'equals', 'hashCode') 를 재정의할 수 있습니다.
이를 통해 각 클래스가 자신만의 독특한 행동을 가지도록 확장할 수 있습니다.
2. 추상화와 다형성의 기초.
OCP를 실현하기 위해서는 추상화와 다형성이 중요한데, 'Object' 클래스는 이 둘의 기초를 제공합니다.
모든 클래스는 'Object' 타입으로 참조될 수 있으므로, OCP를 구현할 때 다형성을 활용할 수 있습니다.
예를 들어, 다양한 타입의 객체를 'Object' 로 처리하면서도, 각 객체가 특정 행위를 다르게 구현하도록 설계할 수 있습니다.
3, OCP 준수에 대한 도움.
'Object' 클래스는 모든 클래스가 공통적으로 가져야 하는 기본적인 기능을 제공하기 때문에, 설계자는 이 기능을 바탕으로 필요한 부분만 재정의하여 기능을 확장할 수 있습니다.
이는 OCP를 준수하는 데 도움이 됩니다.
예를 들어, 특정 객체가 'equals' 메서드를 새롭게 구현함으로써 비교 방식을 확장하면서도, 기존의 'Object' 클래스 코드를 수정할 필요는 없습니다.
4️⃣ 결론.
'Object' 클래스는 모든 클래스의 기반이 되며, OCP를 준수하는 소프트웨어 설계에 있어 중요한 역할을 합니다.
Object 는 직접적으로 OCP를 구현하지 않지만, 그 위에서 개발자들이 OCP 원칙을 적용할 수 있도록 추상화와 다형성의 기초를 제공합니다.
OCP를 준수하는 설계를 통해 개발자는 객체 지향 프로그래밍의 이점을 극대화할 수 있습니다.
-
☕️[Java] Object와 OCP - 1
☕️[Java] Object와 OCP - 1.
1️⃣ Object와 OCP.
만약 'Object' 가 없고, 또 'Object' 가 제공하는 'toString()' 이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다.
여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻입니다.
아마도 다음의 'BadObjectPrinter' 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다.
'BadObjectPrinter'
public class BadObjectPrinter {
public static void print(Car car) { // Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { // Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); //dogInfo() 메서드 만듬
System.out.println(string);
}
}
1️⃣ 구체적인 것에 의존.
'BadObjectPrinter' 는 구체적인 타입인 'Car' , 'Dog' 를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 'BadObjectPrinter' 클래스가 구체적인 특정 클래스인 'Car' , 'Dog' 를 사용하는 것을 'BadObjectPrinter' 는 'Car' , 'Dog' 에 의존한다고 표현합니다.
자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 'Object' 클래스와 메서드 오버라이딩 문제를 해결해줄 'Object.toString()' 메서드가 있습니다.
(물론 직접 'Object' 와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
2️⃣ 추상적인 것에 의존.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
위의 'ObjectPrinter' 클래스는 'Car' , 'Dog' 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 'Object' 클래스를 사용합니다.
이렇게 'ObjectPrinter' 클래스가 'Object' 클래스를 사용하는 것을 'Object' 클래스에 의존한다고 표현합니다.
'ObjectPrinter' 는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다.
Animal 과 Dog , Cat 의 관계를 떠올려봅시다.
Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog , Cat 과 같이 하위 타입으로 내려갈 수록 개념은 더 구체적이게 됩니다.
'ObjectPrinter' 와 'Object' 를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
🤔 ObjectPrinter 의 print() 메서드와 전체 구조 분석.
다형적 참조
'print(Object obj)' , 'Object' 타입을 매개변수로 사용해서 다형적 참조를 사용합니다.
'Car' , 'Dog' 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다.
메서드 오버라이딩
'Object' 는 모든 클래스의 부모입니다.
따라서 'Dog' , 'Car' 와 같은 구체적인 클래스는 'Object' 가 가지고 있는 'toString()' 메서드를 오버라이딩 할 수 있습니다.
따라서 print(Object obj) 메서드는 Dog , Car 와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 'toString()' 을 호출할 수 있습니다.
3️⃣ OCP 원칙.
Open : 새로운 클래스를 추가하고, 'toString()' 을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 'Object' 와 'toString()' 을 사용하는 클라이언트 코드인 'ObjectPrinter' 는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 'Car', 'Dog' 에 의존하는 것이 아니라 추상적인 'Object' 에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter 는 변경할 필요가 없습니다.
'ObjectPrinter' 는 모든 타입의 부모인 'Object' 를 사용하고, 'Object' 가 제공하는 'toString()' 메서드만 사용합니다.
따라서 'ObjectPrinter' 를 사용하면 세상의 모든 객체의 정보('toString()') 를 편리하게 출력할 수 있습니다.
2️⃣ System.out.println()
지금까지 설명한 'ObjectPrinter.print()' 는 사실 'System.out.println()' 의 작동 방식을 설명하기 위해 만든 것입니다.
'System.out.println()' 메서드도 'Object' 매개변수를 사용하고 내부에서 'toString()' 을 호출합니다.
따라서 'System.out.println()' 를 사용하면 세성의 모든 객체의 정보('toString()')를 편리하게 출력할 수 있습니다.
자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다.
우리가 지금까지 배운 'toString()' 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있습니다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계 는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다.
앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다.
쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다.
동적 의존관계 는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다.
앞서 'ObjectPrinter.print(Object obj)' 에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다.
어떤 경우에는 'Car' 인스턴스가 넘어오고, 어떤 경우에는 'Dog' 인스턴스가 넘어옵니다.
이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다.
참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다.
예) 'ObjectPrinter' 는 'Object' 에 의존합니다.
-
-
🍃[Spring] 일반적인 웹 애플리케이션 계층 구조와 클래스 의존관계.
🍃[Spring] 일반적인 웹 애플리케이션 계층 구조와 클래스 의존관계.
1️⃣ 일반적인 웹 애플리케이션 계층 구조.
Controller : 웹 MVC의 Controller 역할.
사용자의 요청을 받아 이를 처리할 비즈니스 로직(서비스 레이어)에 전달하고, 그 결과를 다시 사용자에게 응답하는 역할을 합니다.
주로 HTTP 요청을 처리하고, 올바른 응답을 생성합니다.
컨트롤러는 사용자로부터 입력을 받아 해당 입력을 서비스 레이어로 전달하고, 서비스 레이어에서 처리된 결과를 사용자에게 반환합니다.
이는 주로 웹 애플리케이션의 엔트포인트(예: '/login', '/signup' 와 같은 URL)에 대응됩니다.
Service : 핵심 비즈니스 로직 구현.
비즈니스 로직을 처리하는 계층입니다.
컨트롤러와 리포지토리 사이에서 중간 역할을 하며, 여러 리포지토리로부터 데이터를 가져오거나 가공하고, 이를 다시 컨트롤러에 전달합니다.
서비스 계층은 애플리케이션의 핵심 비즈니스 로직이 위치하는 곳입니다.
예를 들어, 사용자 인증, 결제 처리, 이메일 전송 등의 주요 기능이 이 계층에서 처리됩니다.
Repository: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리.
데이터베이스와 상호작용하는 계층입니다.
데이터의 저장, 검색, 갱신, 삭제 등의 작업을 처리하며, 데이터베이스와의 직접적인 통신을 담당합니다.
리포지토리는 데이터를 처리하기 위한 SQL 쿼리나 ORM(Object-Relational Mapping) 작업을 담당합니다.
이 계층은 서비스 계층에서 필요한 데이터를 가져오거나, 새 데이터를 저장하는 역할을 합니다.
Domain: 비즈니스 도메인 객체.
예를 들어 회원, 주문 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨.
애플리케이션의 핵심 엔티티(Entity)와 비즈니스 규칙을 정의하는 계층입니다.
보통 객체로 표현되며, 비즈니스 로직의 일부를 캡슐화합니다.
도메인 계층은 애플리케이션에서 중요한 객체들(예: 'User', 'Product', 'Order' 등)을 정의하고, 이 객체들이 어떤 방식으로 상호작용하는지를 나타냅니다.
이는 애플리케이션이 어떤 비즈니스 문제를 해결하는지에 대한 모델을 나타냅니다.
2️⃣ 클래스 의존관계.
회원 비즈니스 로직에는 회원 서비스가 있다.
회원을 저장하는 것은 인터페이스로 설계 되어있다.
그 이유는 아직 데이터 저장소가 선정되지 않았음을 가정하고 설계했기 때문이다.
그리고 구현체를 우선은 메모리 구현체로 만들것이다.
그 이유는 일단 개발은 해야하므로 굉장히 단순한 메모리 기반의 데이터 저장소를 사용하여 메모리 구현체로 만든다.
향후에 메모리 구현체를 구체적인 기술이 선정이 되면(RDB, NoSQL 등) 교체할 것이다.
교체하려면 Interface가 필요하므로 Interface를 정의한 것이다.
아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
-
-
-
🌐[Network] 인터네트워킹
🌐[Network] 인터네트워킹
네트워크와 네트워크의 연결을 인터네트워킹(Internetworking)이라 하며, 연결되는 네트워크 수가 증가할수록 복잡도가 커집니다.
인터넷은 IP 프로토콜을 지원하는 전 세계의 모든 네트워크가 반복 구조로 연결된 시스템을 의미하며, 라우터라는 중개 장비를 사용해서 네트워크들을 연결합니다.
1️⃣ 네트워크의 연결
위 그림처럼 서로 독립적으로 운영되는 2개 이상의 네트워크가 연동되어 정보를 교환하려면, 이를 적절히 연결하여 데이터를 중개할 수 있는 인터네트워킹 시스템이 필요합니다.
여기에서 네트워크가 연동된다는 의미는 물리적인 연결뿐 아니라, 데이터 중개에 필요한 상위의 네트워크 프로토콜들이 지원됨을 뜻합니다.
인터넷에서 인터네트워킹 시스템의 주요 기능은 전송 데이터의 경로 선택과 관계가 있습니다.
예를 들어 위 그림의 네트워크 1에서 유입된 데이터를 네트워크 2와 네트워크 3의 누구에게 보낼 것인가를 선택해야 합니다.
이 기능은 7계층 모델에서 네트워크 계층에 포함되므로 인터네트워킹 시스템은 네트워크 계층을 포함한 하위 3개 계층의 기능을 수행합니다.
그림의 예는 일반 도로로 비유하자면 삼거리의 경우와 유사하며, 여기서 자동차 내비게이션 기능이 인터네트워킹 시스템의 주요 기능에 해당합니다.
인터넷의 내부 구조는 이와 같은 인터네트워킹 시스템들이 복잡하게 연결되어 상호 유기적인 협조 체제로 동작합니다.
인터네트워킹 시스템에 연결된 네트워크들은 물리적으로 같은 종류일 필요가 없으며, 상위 계층 프로토콜들이 지원하는 논리적 기능도 다를 수 있습니다.
하지만 인터네트워킹 시스템은 연결된 모든 네트워크에 대하여 물리적이고 논리적인 인터페이스를 모두 지원해야 합니다.
즉, 위 그림에서 인터네트워킹 시스템은 네트워크 1, 네트워크 2, 네트워크 3과 개별적으로 연동할 수 있어야 합니다.
또한 이 과정에서 데이터 표현 방식을 포함해 양쪽 네트워크의 프로토콜이 서로 일치하지 않으면 필요한 변환 작업을 수행해야 합니다.
이러한 방식으로 인터네트워킹 시스템은 둘 이상의 네트워크를 유기적으로 연동할 수 있습니다.
2️⃣ 게이트웨이
인터네트워킹 시스템은 용어 자체로 의미를 쉽게 설명하고 이해시키기 위한 개념적인 명칭이며, 인터네트워킹 기능을 수행하는 시스템을 일반적으로 게이트웨이(Gateway)라 부릅니다.
게이트웨이의 종류는 다양하지만, 일반적으로 지원할 수 있는 기능의 한계에 따라 리피터, 브리지, 라우터 등으로 나뉩니다.
1️⃣ 리피터(Repeater)
리피터(Repeater)는 물리 계층의 기능을 지원합니다.
물리적 신호는 전송 거리가 멀수록 감쇄되기 때문에 중간에 이를 보완해주어야 합니다.
예를 들어, 사람의 목소리는 멀리 전달될수록 세기가 약해져서 점점 알아들을 수 없게 됩니다.
이와 같이 네트워크에서도 무선 신호 혹은 유선의 전기적 신호도 거리가 멀어질수록 신호의 크기가 약해집니다.
따라서 리피터는 한쪽에서 입력된 신호를 물리적으로 단순히 증폭하여 다른 쪽으로 중개하는 역할을 합니다.
2️⃣ 브리스(Bridge)
리피터는 단순히 신호를 증폭하는 역할을 하며, 전송과정에서 발생하는 물리적인 오류 문제는 다루지 않습니다.
이를 보완한 브리지(Bridge)는 리피터 기능에 데이터 링크 계틍의 기능이 추가 된 것으로 물리 계층에서 발생한 오류를 해결해줍니다.
예를 들어, 가정에서 사용하는 무선 공유기는 유무선 기능을 모두 지원하는 브리지의 예입니다.
3️⃣ 라우터(Router)
라우터(Router)는 물리 계층, 데이터 링크 계층, 네트워크 계층의 기능을 지원합니다.
네트워크 계층은 경로 선택 기능을 제공해야 하므로 임의의 네트워크에서 들어온 데이터를 어느 네트워크로 전달할지 판단할 수 있어야 합니다.
이를 위하여 라우터는 자신과 연결된 네트워크와 호스트들의 정보를 유지,관리함으로써 특정 경로가 이용 가능한지 여부와 다수의 경로 중에서 어느 경로가 빠른 데이터 전송을 지원하는지 판단할 수 있어야 합니다.
네트워크와 호스트에 대한 정보는 일반적으로 라우팅 테이블(Routing Table)에 보관됩니다.
-
-
☕️[Java] toString()
☕️[Java] toString()
1️⃣ toString()
'Object.toString()' 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 'Object' 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
package langReview.object.tostring;
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
// toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@b4c966a
java.lang.Object@b4c966a
1️⃣ Object.toString()
'Object' 가 제공하는 'toString()' 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다.
2️⃣ println()과 toString()
'toString()' 의 결과를 출력한 코드와 'object' 를 'println()' 에 직접 출력한 코드의 결과가 완전히 같습니다.
Object object = new Object();
String string = object.toString();
// toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
'System.out.println' 메서드는 사실 내부에서 'toString()' 을 호출합니다.
'Object' 타입(자식 포함)이 'println()' 에 인수로 전달되면 내부에서 'obj.toString()' 메서드를 호출해서 결과를 출력합니다.
따라서 'println()' 을 사용할 때, 'toString()' 을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
2️⃣ toString() 오버라이딩.
'Object.toString()' 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 'toString()' 을 재정의(Overriding, 오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package langReview.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package langReview.object.tostring;
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
package langReview.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package langReview.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("=================================");
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("=================================");
System.out.println("3. Object 다형성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
2. println 내부에서 toString 호출
langReview.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
=================================
3. Object 다형성 활용
객체 정보 출력: langReview.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
'Car' 인스턴스는 'toString()' 을 재정의 하지 않았습니다.
따라서 'Object' 가 제공하는 기본 'toString()' 메서드를 사용합니다.
'Dog' 인스턴스는 'toString()' 을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
1️⃣ ObjectPrinter.print(Object obj) 분석 - Car 인스턴스
ObjectPrinter.print(car);
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'car(Car)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
재정의된 메서드가 없을 경우에는 'Object.toString()' 을 실행합니다.
2️⃣ ObjectPrinter.print(Object obj) 분석 - Dog 인스턴스
ObjectPrinter.print(dog); // main에서 호출
void print(Object obj = car(Car)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
'Object obj' 의 인수로 'dog(Dog)' 가 전달됩니다.
메서드 내부에서 'obj.toString()' 을 호출합니다.
'obj' 는 'Object' 타입입니다.
따라서 'Object' 에 있는 'toString()' 을 찾습니다.
이때 자식에 재정의(Overriding, 오버라이딩)된 메서드가 있는지 찾아봅니다.
'Dog' 에 재정의된 메서드가 있습니다.
'Dog.toString()' 을 실행합니다.
🙋♂️ 참고 - 객체의 참조값 직접 출력
'toString()' 은 기본으로 객체의 참조값을 출력합니다.
그런데 'toString()' 이나 'hashCode()' 를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);
실행 결과
refValue = 30dae81
-
-
💾 [CS] 싱글톤 패턴
💾 [CS] 싱글톤 패턴.
1️⃣ 싱글톤 패턴(Singleton pattern)
싱글톤 패턴(singleton pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다.
하나의 클래스를 기반으로 여러 개의 개별적인 인스턴스를 만들 수 있지만, 그렇게 하지 않고 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는데 쓰입니다.
보통 데이터베이스 연결 모듈에 많이 사용합니다.
하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있습니다.
하지만 의존성이 높아진다는 단점이 있습니다.
2️⃣ Java에서의 싱글톤 패턴.
Java에서 Singleton 패턴을 구현하는 방법은 여러 가지가 있지만, 가장 일반적으로 사용되는 방법 중 몇 가지를 소개하겠습니다.
Eager Initialization(즉시 초기화)
Lazy Initialization(지연 초기화)
Thread-safe Singleton(스레드 안전 싱글톤)
Synchronized Method
Double-checked Locking
Bill Pugh Singleton(Holder 방식)
1️⃣ Eager Initialization(즉시 초기화)
가장 간단한 방법으로, 클래스가 로드될 때 즉시 Singleton 인스턴스를 생성합니다.
public class Singleton {
// 유일한 인스턴스 생성
private static final Singleton instance = new Singleton();
// private 생성자: 외부에서 인스턴스 생성을 방지
private Singleton() {}
// 인스턴스를 반환하는 메서드
public static Singleton getInstance() {
return instance;
}
}
이 방법은 간단하고 직관적이지만, 클래스가 로드될 때 바로 인스턴스가 생성되기 때문에, 인스턴스가 사용되지 않더라도 메모리를 차지하게 됩니다.
2️⃣ Lazy Initialization(지연 초기화)
인스턴스가 처음으로 필요할 때 생성되도록 합니다.
이 방법은 초기화에 드는 비용이 큰 경우 유리합니다.
```java
public class Singleton {
// 유일한 인스턴스를 저장할 변수 (초기에는 null)
private static Singleton instance;
// private 생성자: 외부에서 인스턴스 생성을 방지
private Singleton() {}
// 인스턴스를 반환하는 메서드 (필요할 때만 생성)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
```
이 방법은 다중 스레드 환경에서 안전하지 않기 때문에, 추가적인 동기화가 필요합니다.
3️⃣ Thread-safe Singleton(스레드 안전 싱글톤)
다중 스레드 환경에서 안전하게 Lazy Initialization을 구현하려면 동기화를 사용합니다.
1️⃣ Synchronized Method
public class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized 키워드로 스레드 안전하게 만듦
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이 방법은 안전하지만, 성능에 약간의 영향을 줄 수 있습니다.
'synchronized' 로 인해 여러 스레드가 동시에 ‘getInstance()‘ 를 호출할 때 병목 현상이 발생할 수 있습니다.
2️⃣ Double-checked Locking
이 방법은 성능과 스레드 안전성을 모두 고려한 최적화된 방식입니다.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
여기서 'volatile' 키워드는 인스턴스 변수가 스레드 간에 올바르게 초기화되도록 보장합니다.
4️⃣ Bill Pugh Singleton(Holder 방식)
이 방법은 Lazy Initialization을 사용하면서도, 성능과 스레드 안전성을 모두 보장합니다.
public class Singleton {
private Singleton() {}
// SingletonHolder가 클래스 로드 시점에 초기화됨
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
이 방법은 내부 정적 클래스가 JVM에 의해 클래스 로드 시 초기화되므로, 가장 권장되는 방식 중 하나입니다.
클래스가 로드될 때 초기화가 이루어지므로, 동기화나 추가적인 코드 없이도 스레드 안전성을 보장할 수 있습니다.
3️⃣ Spring Boot와 MySQL 데이터베이스의 연결 그리고 싱글턴 패턴.
Spring Boot에서 MySQL 데이터베이스를 연결할 때, 내부적으로 ‘싱글턴 패턴’ 이 사용됩니다.
그러나 이 패턴을 직접 구현할 필요는 없습니다.
‘Spring Framework’ 자체가 싱글턴 패턴을 활용하여 데이터베이스 연결 및 관리와 관련된 ‘Bean(객체)’ 을 관리합니다.
1️⃣ Spring Boot와 싱글턴 패턴.
Spring Framework는 기본적으로 각 Bean을 싱글턴 스코프로 관리합니다.
이는 특정 클래스의 인스턴스가 애플리케이션 컨텍스트 내에서 한 번만 생성되어 애플리케이션 전반에서 공유됨을 의미합니다.
2️⃣ 데이터베이스 연결에서의 싱글턴 패턴 사용.
DataSource Bean.
Spring Boot에서 MySQL과 같은 데이터베이스에 연결할 때 'DataSource' 라는 'Bean' 을 생성하여 관리합니다.
이 'DataSource' 객체는 데이터베이스 연결을 관리하는 역할을 하며, Spring은 이 'Bean' 을 싱글턴으로 생성하고 관리합니다.
즉, Spring 애플리케이션 내에서는 'DataSource' 객체가 하나만 생성되어 모든 데이터베이스 연결 요청에서 재사용됩니다.
EntityManagerFactory 및 SessionFactory.
JPA나 Hibernate와 같은 ORM을 사용하는 경우, 'EntityManagerFactory' 나 'SessionFactory' 와 같은 객체도 싱글턴 패턴에 의해 관리됩니다.
이들 객체는 데이터베이스 연결을 처리하고 트랜잭션을 관리하며, 역시 Spring에 의해 싱글턴으로 관리됩니다.
Spring의 싱글턴 관리.
Spring은 개발자가 'Bean' 을 직접 싱글턴으로 관리할 필요가 없도록, 애플리케이션의 컨텍스트 내에서 'Bean' 을 싱글턴으로 관리합니다.
데이터베이스와의 연결 관련 클래스들이 이 'Bean' 들로 구성되며, 이는 데이터베이스 연결이 효율적이고 일관되게 관리되도록 보장합니다.
3️⃣ 예시: Spring Boot에서 MySQL 연결 설정.
Spring Boot에서 MySQL 데이터베이스를 연결하기 위한 일반적인 설정은 'application.properties' 파일이나 'application.yml' 파일에 데이터베이스 연결 정보를 추가하는 것입니다.
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
이 설정은 Spring Boot가 'DataSource' 'Bean' 을 자동으로 생성하도록 하며, 이 'Bean' 은 애플리케이션 내에서 싱글턴으로 관리됩니다.
4️⃣ ✏️ 요약
Spring Boot에서 MySQL과 같은 데이터베이스를 연결할 때, Spring은 내부적으로 싱글턴 패턴을 사용하여 데이터베이스 연결을 관리합니다.
'DataSource', 'EntityManagerFactory' 등의 객체가 싱글턴으로 관리되며, 이를 통해 애플리케이션 전반에 걸쳐 일관되고 효율적인 데이터베이스 연결 관리가 이루어집니다.
Spring 자체가 이 패턴을 처리하므로, 개발자는 별도로 싱글턴 패턴을 구현할 필요가 없습니다.
4️⃣ Java Servlet 컨테이너와 MySQL 데이터베이스 연결 그리고 싱글턴 패턴.
Java Servlet 컨테이너에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴이 일반적으로 사용됩니다.
다만, 이 패턴은 애플리케이션 코드에서 직접 구현되는 것이 아니라, 서블릿 컨테이너나 데이터베이스 연결 관리 라이브러리에서 사용됩니다.
1️⃣ JDBC DataSource
서블릿 컨테이너(예: Tomcat, Jetty)에서 데이터베이스 연결을 설정할 때 보통 'DataSource' 를 사용합니다.
이 'DataSource' 객체는 보통 싱글턴으로 관리되며, 데이터베이스 연결 풀을 제공합니다.
Connection Pooling
서블릿 컨테이너는 데이터베이스 연결을 관리하기 위해 연결 풀링(Connection pooling)을 사용합니다.
연결 풀은 여러 데이터베이스 연결을 미리 생성하고 재사용하도록 관리합니다.
연결 풀을 관리하는 객채는 'DataSource' 이고, 이는 애플리케이션 내에서 싱글턴으로 관리되어, 여러 서블릿에서 동일한 'DataSource' 객체를 사용하여 효율적으로 데이터베이스에 연결할 수 있습니다.
2️⃣ 싱글턴 패턴의 활용.
DataSource 객체
서블릿 컨테이너는 보통 'DataSource' 객체를 싱글턴으로 관리합니다.
'DataSource' 는 데이터베이스 연결 풀을 관리하며, 이 객체가 한 번만 생성되어 애플리케이션 전반에 걸쳐 재사용됩니다.
Connection 객체
각 요청마다 데이터베이스 연결이 필요할 때마다 새로운 'Connection' 객체가 생성되거나 풀에서 가져오게 됩니다.
하지만 'DataSource' 자체는 싱글턴으로 관리되기 때문에, 동일한 'DataSource' 객체를 통해 연결이 이루어집니다.
3️⃣ 예시: Tomcat에서 DataSource 설정
Tomcat과 같은 서블릿 컨테이너에서 MySQL 데이터베이스와의 연결을 설정하는 일반적인 방법은 'context.xml' 파일에서 'DataSource' 를 정의하는 것입니다.
<Context>
<Resource name="jdbc/MyDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdel="30"
maxWaitMillis="10000"
username="root"
password="password"
driverClassName="com.myslq.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb"/>
</Context>
이 설정은 'jdbc/MyDB' 라는 JNDI 리소스를 정의하고, 'DataSource' 객체를 생성하여 연결 풀링을 관리합니다.
이 'DataSource' 는 Tomcat 내에서 싱글톤으로 관리됩니다.
4️⃣ 싱글턴 패턴의 이점.
효율성.
여러 서블릿이 동일한 'DataSource' 객체를 공유함으로써 메모리와 자원을 절약할 수 있습니다.
관리의 용이성.
데이터베이스 연결 관리를 중앙화할 수 있으며, 코드에서 직접 관리할 필요 없이 서블릿 컨테이너가 이를 담당합니다.
5️⃣ ✏️ 요약
Java Servlet 컨테이너에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 주로 DataSource 객체에 적용됩니다.
이 DataSource 객체는 서블릿 컨테이너에 의해 싱글턴으로 관리되며, 데이터베이스 연결 풀을 통해 효율적으로 데이터베이스 연결을 처리합니다.
이를 통해 애플리케이션 전반에 걸쳐 일관되고 성능이 최적화된 데이터베이스 연결 관리가 이루어집니다.
3️⃣ Java 애플리케이션 서버와 MySQL 데이터베이스의 연결 그리고 싱글턴 패턴.
Java 애플리케이션 서버에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 우요한 역할을 합니다.
그러나 이 패넡은 애플리케이션 코드에서 직접 구현되지 않으며, 애플리케이션 서버나 데이터베이스 연결 관리 라이브러리에서 사용됩니다.
1️⃣ DataSource와 Connection Pooling
Java 애플리게이션 서버(예: JBoss/WildFly, GlassFish, WebSphere)에서 데이터베이스를 연결할 때 일반적으로 'JDBC DataSource' 와 'Connection Pooling' 을 사용합니다.
이때 DataSource 객체는 싱글턴으로 관리되며, 데이터베이스 연결의 효율성을 높이기 위해 연결 풀을 사용합니다.
DataSource 싱글턴 관리
애플리케이션 서버는 데이터베이스와의 연결을 관리하기 위해 DataSource를 생성합니다.
이 DataSource 객체는 서버에서 싱글턴으로 관리됩니다.
즉, 애플리케이션 전반에 걸쳐 동일한 DataSource 객체가 사용됩니다.
DataSource는 내부적으로 데이터베이스 연결 풀을 관리하며, 여러 클라이언트 요청에서 동일한 데이터베이스 연결 객체를 재사용합니다.
Connection 객체 관리
데이터베이스와의 실제 연결을 관리하는 Connection 객체는 매번 새로운 요청이 있을 때마다 DataSource에서 가져오지만, DataSource는 싱글턴으로 관리되므로 전체 애플리케이션에서 일관된 연결 풀이 사용됩니다.
2️⃣ Java EE 환경에서의 DataSource 관리
Java EE 애플리케이션 서버에서는 'JNDI(Java Naming and Directory Interface)' 를 통해 DataSource를 관리합니다.
이는 서버의 전역 설정에서 관리되며, 여러 애플리케이션이 동일한 데이터베이스 연결을 공유할 수 있도록 합니다.
JNDI를 통한 DataSource 설정 예시
```xml
- 이 설정은 애플리케이션 서버가 싱글턴 DataSource 객체를 생성하고 관리하도록 합니다.
### 3️⃣ 싱글턴 패턴의 역할.
- **효율성**
- 싱글턴으로 관리되는 DataSource는 애플리케이션 서버 전체에서 하나의 객체로 유지되며, 이를 통해 메모리와 자원 사용이 최적화됩니다.
- **일관성**
- 동일한 데이터베이스 연결 풀을 사용하기 때문에 애플리케이션 전방에 걸쳐 데이터베이스 연결이 일관되게 관리됩니다.
- **관리 용이성**
- 데이터베이스 연결 관리가 중앙화되어, 각 애플리게이션에서 따로 관리할 필요 없이 서버에서 통합 관리됩니다.
### 4️⃣ EJB와의 통합.
- JavaEE 환경에서 EJB(Enterprise JavaBeans)는 주로 애플리케이션 서버에서 관리되는 비즈니스 로직을 구현하는 데 사용됩니다.
- EJB에서 데이터베이스 연결을 사용할 때도 싱글턴 패턴이 적용된 DataSource를 통해 연결이 이루어집니다.
```java
@Stateless
public class MyService {
@Resource(lookup = "java:/jdbc/MyDB")
private DataSource dataSource;
public void doSomething() {
try (Connection connection = dataSource.getConnection()) {
// 데이터베이스 작업 수행
} catch (SQLException e) {
e.printStackTrace();
}
}
}
이 코드에서 'dataSource' 는 서버에 의해 관리되는 싱글턴 DataSource 객체를 참조하며, 이를 통해 데이터베이스 연결을 처리합니다.
5️⃣ ✏️ 요약,
Java 애플리케이션 서버에서 MySQL 데이터베이스를 연결할 때, 싱글턴 패턴은 DataSource와 같은 중요한 객체 관리에 사용됩니다.
이 패턴을 통해 애플리케이션 서버는 데이터베이스 연결을 효율적이고 일관되게 관리할 수 있으며, 연결 풀링을 통해 자원 사용을 최적화합니다.
애플리케이션 서버가 DataSource를 싱글턴으로 관리함으로써, 서버 전반에 일관된 데이터베이스 연결을 제공하고 효율성을 극대화할 수 있습니다.
-
🍃[Spring] slf4j와 logback.
🍃[Spring] slf4j와 logback.
1️⃣ slf4j
'SLF4J(Simple Logging Facade for Java)' 는 Java 애플리케이션에서 로그 기록을 쉽게 관리하고 다른 로깅 프레임워크와 통합할 수 있도록 도와주는 로깅 인터페이스입니다.
'SLF4J' 는 다양한 로깅 프레임워크(e.g, Log4j, Logback, java.util.logging 등)에 대해 공통된 인터페이스를 제공하여 개발자가 특정 로깅 프레임워크에 종속되지 않고 유연하게 로그를 관리할 수 있도록 합니다.
1️⃣ slf4j의 주요 기능.
로깅 프레임워크와의 추상화
slf4j는 여러 로깅 프레임워크에 종속되지 않게 합니다.
예를 들어, 코드에서 slf4j 인터페이스를 사용하면 나중에 로깅 프레임워크를 쉽게 교체할 수 있습니다.
로깅 성능 최적화
slf4j는 문자열 병합에 따른 성능 문제를 피할 수 있도록 지원합니다.
예를 들어, slf4j는 로그 메시지의 문자열 결합을 지연시켜, 로그가 실제로 기록될 때만 결합이 발생하도록 합니다.
API 일관성
slf4j를 사용하면 로깅을 위한 일관된 API를 제공받을 수 있으며, 이를 통해 로깅을 표준화할 수 있습니다.
2️⃣ 사용 방법.
slf4j를 사용하기 위해서는, 우선 slf4j 인터페이스와 이를 구현한 로깅 프레임워크(예: Logback)를 프로젝트에 포함시켜야 합니다.
코드는 일반적으로 아래와 같이 사용됩니다.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
// Logger 생성
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void doSomthing() {
// 로그 메시지 기록
logger.info("This is an info message");
logger.debug("This is a debug message");
} } ``` - 이 코드는 **`'slf4j'`** 를 이용해 로그를 기록하는 예로, 로깅 메시지는 설정된 로깅 프레임워크를 통해 출력됩니다.
✏️ 요약.
slf4j는 Java 애플리케이션에서 로깅 프레임워크 간의 추상화 레이어를 제공하며, 코드가 특정 로깅 프레임워크에 종속되지 않도록 합니다.
이를 통해 유연한 로깅 관리가 가능해집니다.
2️⃣ logback
'logback' 은 Java 애플리케이션에서 사용되는 고성능 로깅 프레임워크로, slf4j의 권장 구현체 중 하나입니다.
'logback' 은 slf4j를 통해 접근할 수 있으며, 뛰어난 성능과 유연한 설정, 다양한 기능을 제공하는 것이 특징입니다.
1️⃣ logback의 주요 구성 요소.
Logback Classic
slf4j와 직접 통합되는 logback의 핵심 모듈입니다.
'Logback Classic' 은 Java 애플리케이션에서 로깅 기능을 수행하며, 다양한 로그 레벨(INFO, DEBUG, WARN, ERROR 등)을 지원합니다.
Logback Core
Logback Classic과 Logback Access(웹 애플리케이션용)를 기반으로 하는 일반적인 로깅 기능을 제공합니다.
'Logback Core' 는 Appender, Layout, Filter 등과 같은 기본 구성 요소를 포함합니다.
Logback Access
웹 애플리케이션에서 HTTP 요청과 응답을 로깅할 수 있도록 지원하는 모듈입니다.
주로 Java Servlet 환경에서 사용됩니다.
3️⃣ logback의 특징.
높은 성능
'logback' 은 빠른 로깅 성능을 제공하며, 특히 대규모 애플리케이션에서 효과적입니다.
유연한 구성
'logback' 은 XML 또는 Groovy 스크립트로 로깅 설정을 구성할 수 있습니다.
이를 통해 다양한 조건에 따라 로깅 동작을 세밀하게 제어할 수 있습니다.
조건부 로깅
'logback' 은 특정 조건에서만 로깅을 수행하도록 설정할 수 있어, 불필요한 로그 기록을 줄이고 성능을 최적화할 수 있습니다.
이전 로그 프레임워크와의 호환성
'logback' 은 기존의 'Log4j' 설정 파일을 사용할 수 있는 기능을 제공하여, 기존 'Log4j' 사용자가 쉽게 'logback' 으로 전환할 수 있도록 돕습니다.
다양한 출력 형식
'logback' 은 콘솔, 파일, 원격 서버, 데이터베이스 등 다양한 출력 대상으로 로그를 기록할 수 있으며, 출력 형식을 자유롭게 정의할 수 있습니다.
4️⃣ logback 사용 예제.
<configuration>
<!-- 콘솔에 로그를 출력하는 Appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 파일에 로그를 기록하는 Appender -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>mylog.log</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 루트 로거 설정 -->
<root level="debug">
<appender-ref ref="STDOUT" />
<appender-red red="FILE" />
</root>
</configuration>
이 예시는 콘솔과 파일에 로그를 출력하도록 설정하는 간단한 예시입니다.
logback은 이외에도 복잡한 요구 사항을 충족할 수 있는 다양한 기능을 제공하고 있습니다.
✏️ 요약.
logback은 Java 애플리케이션에서 사용되는 고성능 로깅 프레임워크로, slf4j와 함께 사용됩니다.
logback은 유연한 설정과 높은 성능, 다양한 기능이 있습니다.
-
-
-
-
-
-
-
🌐[Network] 프로토콜과 인터페이스(Protocol and Interface)
🌐[Network] 프로토콜과 인터페이스(Protocol and Interface)
1️⃣ 프로토콜과 인터페이스(Protocol and Interface).
네트워크 사용자가 통신한다는 것은 데이터를 서로 주고받는다는 것을 의미합니다.
최종 사용자가 데이터를 보내고 받으려면 양쪽 호스트에서 실행되는 OSI 7계층의 모듈이 유기적으로 연동되어야 합니다.
즉, 호스트끼리 통신하는 과정에서는 각 계층의 모듈이 상대 호스트의 동일 계층과 개별적으로 논리적 통신을 수행해야 합니다.
예를 들어, 통신 양단의 한쪽 호스트의 계층 n 모듈은 상대 호스트의 계층 n 모듈과 통신합니다.
이와 같이 각각의 계층은 정해진 방식과 절차에 따라 상대 계층과 통신하는데, “이 과정에서 필요한 규칙을 프로토콜(Protocol)” 이라고 합니다.
“상하위의 계층 간에는 인터페이스(Interface)” 라는 규칙이 존재하고, “하위 계층이 상위 계층에 제공하는 인터페이스를 특별히 서비스(Service)” 라 부릅니다.
위 그림은 계층 n과 계층 n-1의 2개 모듈로 구성된 계층 모델에서 서로 다른 두 호스트의 통신을 지원하기 위한 모듈 간의 관계를 프로토콜, 서비스, 데이터 전송의 관점에서 설명합니다.
한 호스트를 기준으로 데이터 전송은 위아래 양방향으로 모두 가능하며, 두 호스트 사이에서는 좌우 양방향으로 모두 가능합니다.
다만, 좌우 간의 물리적인 데이터 전송은 반드시 가장 아래의 물리 계층을 통하여 이루어집니다.
호스트 1과 호스트 2의 계층 n 프로토콜이 서로 통신하려면 계층 n-1 프로토콜의 서비스가 필요합니다.
즉, 호스트 1의 계층 n이 호스트 2의 계층 n에 데이터를 전송하는 과정은 하위의 계층 n-1을 통해 이루어집니다.
먼저, 호스트 1의 계층 n-1에 전송할 데이터를 주어 호스트 2에 전송하도록 부탁합니다.
그러면 호스트 1의 계층 n-1은 다시 하위 계층의 도움을 받아 호스트 2의 계층 n-1에 데이터를 보냅니다.
마지막으로 호스트 2의 계층 n-1이 수신한 데이터를 계층 n에 올려줌으로써 계층 n 사이의 통신이 완료됩니다.
이 원리는 ISO 7계층 모델에서 7개 계층에 모두 적용되며, 상대 호스트에 물리적으로 데이터를 전송하는 것은 맨 아래의 물리 계층입니다.
물리 계층 위에 있는 계층 프로토콜들은 각자의 정해진 기능을 수행하면서 논리적인 통신을 하는 것입니다.
-
🌐[Network] 네트워크 세그먼트(Network Segment).
🌐 네트워크 세그먼트(Network Segment).
1️⃣ 세그먼트(Segment).
네트워크 세그먼트(Network Segment)를 알아보기 전에 세그먼트(Segment)가 어떤 뜻을 가지고 있는지 궁금했습니다.
TTA 정보통신용어사전에서는 다음과 같이 세그먼트를 정의하고 있었습니다.
서로 구분되는 기억 장치 의 연속된 한 영역.
어떤 프로그램이 너무 커서 한 번에 주기억 장치 에 올라올 수 없이 갈아넣기 기법을 사용하여 쪼개었을 때, 나뉜 각 부분을 가리키는 말.
세그먼테이션 방식의 가상 기억 장치 에서 사용되는 것으로, 페이징에서 페이지와 비슷하나 길이가 가변이고 기억 장치 의 어느 곳에서도 자리할 수 있는 기억 장소 영역을 가리키는 말.
한 세그먼트는 프로그램의 논리적인 한 구성 단위를 저장한다.
계층 모형의 데이터베이스 에서 여러 항목이 모여 레코드에 해당하는 단위.
2️⃣ 네트워크 세그먼트(Network Segment).
네트워크 세그먼트(Network Segment)는 네트워크 내에서 논리적 또는 물리적으로 분리된 하나의 부분 또는 구역을 의미합니다.
네트워크를 여러 세그먼트로 나누는 것은 네트워크의 성능을 최적화하고, 보안을 강화하며, 트래픽 관리를 효율적으로 ㅎ기 위한 일반적인 방법입니다.
1️⃣ 네트워크 세그먼트의 주요 개념.
1. 물리적 세그먼트
물리적 네트워크 세그먼트는 실제 네트워크 장비(스위치, 라우터 등)와 물리적 케이블을 통해 분리된 네트워크 구역입니다.
예를 들어, 한 사무실 내에서 여러 층에 있는 네트워크가 각기 다른 물리적 스위치에 연결되어 있다면, 이들 층은 물리적으로 서로 다른 네트워크 세그먼트로 간주될 수 있습니다.
2. 논리적 세그먼트
논리적 네트워크 세그먼트는 물리적 인프라와 무관하게 네트워크를 논리적으로 나누는 것을 의미합니다.
이는 주로 VLAN(가상 로컬 영역 네트워크) 이나 서브넷(Subnet) 을 사용하여 이루어집니다.
예를 들어, 동일한 물리적 네트워크 내에서 서로 다른 부서의 컴퓨터를 논리적으로 분리하여 독립된 네트워크 세그먼트로 만들 수 있습니다.
2️⃣ 네트워크 세그먼트의 주요 목적.
1. 보안 강화
네트워크 세그먼트는 네트워크를 여러 개의 작은 구역으로 나누어, 각 구역의 트래픽을 독립적으로 관리하고 보호할 수 있습니다.
이를 통해 민감한 데이터를 처리하는 부서나 서버를 외부와 분리하여 보안을 강화할 수 있습니다.
2. 성능 최적화
네트워크를 세그먼트로 나누면 네트워크의 트래픽이 특정 구역 내에서만 흐르게 하여, 트래픽 혼잡을 줄이고 전체 네트워크의 성능을 향상시킬 수 있습니다.
예를 들어, 대용량 파일 전송이 필요한 부서의 네트워크 세그먼트를 다른 부서와 분리함으로써, 다른 부서의 네트워크 성능에 영향을 주지 않도록 할 수 있습니다.
3. 트래픽 관리
네트워크 세그먼트를 통해 트래픽을 효과적으로 관리하고, 특정 세그먼트의 트래픽을 제거할 수 있습니다.
네트워크 관리자는 각 세그먼트에 대해 별도의 라우팅 규칙, 방화벽 규칙 등을 적용할 수 있습니다.
4. 내결함성 향상
네트워크가 세그먼트로 분리되어 있으면, 한 세그먼트에서 발생한 문제가 다른 세그먼트로 확산되지 않도록 방지할 수 있습니다.
예를 들어, 한 세그먼트에서 발생한 네트워크 장애가 전체 네트워크에 영향을 미치지 않게 할 수 있습니다.
3️⃣ 예시: 서브넷을 이용한 네트워크 세그먼트.
서브넷은 네트워크 세그먼트의 한 유형입니다.
예를 들어, ‘10.0.0.0/16’ IP 주소 블록을 가진 VPC에서 ‘10.0.1.0/24’ 와 ‘10.0.2.0/24’ 라는 두 개의 서브넷을 생성할 수 있습니다.
이 두 서브넷은 동일한 VPC 내에서 서로 독립된 네트워크 세그먼트를 구성합니다.
이로 인해 한 서브넷에서 발생한 네트워크 트래픽은 기본적으로 다른 서브넷에 영향을 미치지 않습니다.
✏️ 요약
요약하자면, 네트워크 세그먼트는 네트워크를 보다 안전하고 효율적으로 운영하기 위해 분리된 네트워크 구역을 의미하며, 물리적 또는 논리적으로 구성될 수 있습니다.
-
-
-
-
☁️[AWS] 테넌시(Tenancy)
☁️[AWS] 테넌시(Tenancy).
“테넌시(Tenancy)” 라는 용어는 원래 부동산에서 사용되던 개념으로, 특정 공간을 임차하거나 소유하는 상태를 의미합니다.
“이 개념이 IT와 클라우드 컴퓨팅으로 확장되면서, 테넌시는 리소스나 환경을 특정 사용자나 조직이 사용하거나 소유하는 방식으로 사용됩니다.”
1️⃣ 테넌시의 일반적인 개념.
1️⃣ 부동산에서의 테넌시.
정의
테넌시는 주로 임차인이 집이나 건물을 사용하는 권리를 가지고 있는 상태를 의미합니다.
테넌시는 임대 계약을 통해 특정 기간 동안 특정 부동산을 사용하는 권리를 확보하게 됩니다.
유형
테넌시는 단독 테넌시(한 사람이 독점으로 사용)와 공동 테넌시(여러 사람이 함께 사용) 등으로 구분될 수 있습니다.
2️⃣ IT와 클라우드 컴퓨팅에서의 테넌시.
정의
IT 분야에서 테넌시는 특정 사용자나 조직이 IT 자원(서버, 네트워크, 데이터베이스 등)을 사용하는 상태를 의미합니다.
이 용어는 특히 클라우드 컴퓨팅에서 자주 사용되며, 리소스가 물리적 또는 논리적으로 어떻게 격리되고 공유되는지를 설명하는 데 사용됩니다.
유형
클라우드 컴퓨팅에서 테넌시는 주로 다음 두 가지 유형으로 구분됩니다.
단일 테넌시(Single-Tenancy)
한 명의 사용자나 조직이 독점적으로 자원을 사용하는 환경. 물리적 하드웨어 또는 소프트웨어 환경이 다른 사용자와 공유되지 않습니다.
예를 들어, 전용 서버 또는 전용 호스트 환경이 해당됩니다.
다중 테넌시(Multi-Tenancy)
여러 사용자나 조직이 동일한 물리적 하드웨어 또는 소프트웨어 환경을 공유하는 환경. 하지만 각 사용자의 데이터와 리소스는 논리적으로 격리되어 있습니다.
대부분의 퍼블릭 클라우드 서비스는 다중 테넌시 구조를 채택하고 있습니다.
3️⃣ 테넌시의 중요성.
자원 격리
테넌시는 사용자가 특정 IT 자원을 다른 사용자와 공유할지, 아니면 독립적으로 사용할지를 결정하는 중요한 요소입니다.
이를 통해 보안 수준을 강화하거나 비용을 절감할 수 있습니다.
보안 및 규정 준수
일부 조직은 규제 요구 사항을 충족하기 위해 단일 테넌시를 요구할 수 있습니다.
이 경우 물리적 자원을 다른 조직과 공유하지 않음으로써 보안성을 높일 수 있습니다.
비용 효율성
다중 테넌시는 자원을 공유함으로써 비용을 절감할 수 있습니다.
클라우드 서비스 제공자는 다중 테넌시를 통해 자원을 최적화하고, 이를 통해 사용자가 더 저렴한 비용으로 서비스를 이용할 수 있도록 합니다.
🎯 요약
“테넌시(Tenancy)” 는 특정 자원을 사용자 또는 조직이 사용하는 방식과 관련된 개념입니다.
부동산에서는 임차 관계를 의미합니다.
IT와 클라우드 컴퓨팅에서는 리소스가 어떻게 배치되고 격리되는지를 설명하는 용어로 사용됩니다.
클라우드 환경에서 테넌시의 선택은 보안, 성능, 비용에 큰 영향을 미칩니다.
-
☁️[AWS] 서브넷?
☁️[AWS] 서브넷(Subnet).
AWS애서 서브넷(Subnet)은 VPC(Virtual Private Cloud) 내에서 네트워크를 세분화하고 리소스를 논리적으로 분리하는 방법입니다.
서브넷은 VPC 내의 특정한 IP 주소 범위를 갖는 네트워크 세그먼트를 나타내며, AWS에서 네트워크 인프라를 구축하고 관리하는 데 중요하는 역할을 합니다.
1️⃣ VPC와 서브넷의 관계.
VPC(Virtual Private Cloud)
AWS에서 제공하는 가상 네트워크로, 사용자가 자신의 클라우드 리소스를 배치하고 관리할 수 있는 격리된 네트워크 환경입니다.
VPC는 사용자가 직접 IP 주소 범위, 서브넷, 라우팅 테이블, 네트워크 게이트웨이 등을 설정할 수 있도록 합니다.
서브넷(Subnet)
서브넷은 VPC 내에서 특정 IP 주소 범위를 갖는 작은 네트워크 세그먼트입니다.
“각 서브넷은 VPC의 전체 IP 주소 범위 내에서 특정한 부분을 할당받아 사용합니다.”
2️⃣ 서브넷의 유형.
퍼블릭 서브넷(Public Subnet)
인터넷 게이트웨이(Internet Gateway)에 연결된 서브넷으로, 이 서브넷에 배치된 리소스(예: EC2 인스턴스)는 퍼블릭 IP 주소를 가지며 인터넷과 직접 통시할 수 있습니다.
웹 서버와 같은 인터넷과 직접 연결이 필요한 리소스를 퍼블릭 서브넷에 배치합니다.
프라이빗 서브넷(Private Subnet)
인터넷 게이트웨이에 연결되지 않은 서브넷으로, 이 서브넷에 배치된 리소스는 퍼블릭 IP 주소가 없으며, 직접적으로 인터넷과 통신할 수 없습니다.
데이터베이스 서버와 같이 인터넷과 직접 연결될 필요가 없는 리소스를 프라이빗 서브넷에 배치합니다.
인터넷에 접속해야 할 경우, NAT 게이트웨이 또는 NAT 인스턴스 를 통해 간접적으로 인터넷에 접근할 수 있습니다.
3️⃣ 서브넷의 가용 영역(Availability Zone).
각 서브넷은 특정 가용 영역(AZ)에 속합니다.
이는 가용 영역마다 독립적인 네트워크 세그먼트를 구성할 수 있게 해줍니다.
예를 들어, VPC 내에 여러 가용 영역이 있을 때, 각각의 가용 영역에 서브넷을 만들어 고가용성 아키텍처를 구축할 수 있습니다.
4️⃣ 라우팅 테이블과 서브넷.
각 서브넷은 하나의 라우팅 테이블(Routing Table)과 연결됩니다.
라우팅 테이블은 네트워크 트래픽이 어떻게 라우팅되는지를 결정합니다.
예를 들어, 퍼블릭 서브넷은 라우팅 테이블에 인터넷 게이트웨이로 가는 경로가 설정되어 있지만, 프라이빗 서브넷은 그런 경로가 없습니다.
5️⃣ 서브넷의 보안 그룹 및 네트워크 ACL.
보안 그룹(Security Groups)
서브넷에 배치된 리소스(예: EC2 인스턴스)에 대한 인바운드 및 아웃바운드 트래픽을 제어하는 가상 방화벽입니다.
보안 그룹은 상태 기반으로 동작하며, 인스턴스 수준에서 적용됩니다.
네트워크 ACL(Network Access Control List)
서브넷 수준에서 적용되는 보안 레이어로, 서브넷 내의 모든 리소스에 대한 트래픽을 제어합니다.
네트웨크 ACL은 상태 비저장(State-less)으로, 각각의 요청과 응답을 별도로 처리합니다.
6️⃣ 서브넷의 사용 사례.
웹 서버 및 애플리케이션 서버 배치
퍼블릭 서브넷에 배치하여 외부에서 접근 가능한 웹 서버와 애플리케이션 서버를 운영합니다.
데이터베이스 서버 및 내부 애플리케이션 배치
프라이빗 서브넷에 배치하여 외부 접근이 제한된 안전한 네트워크 환경에서 데이터베이스 서버나 비공개 애플리케이션을 운영합니다.
멀티 AZ 아키텍처
각 가용 영역에 서브넷을 만들어 장애가 발생해도 시스템이 지속적으로 운영될 수 있는 고가용성 아키텍처를 구축합니다.
7️⃣ 서브넷과 CIDR 블록.
각 서브넷은 CIDR(Classless Inter-Domain Routing) 블록으로 정의된 IP 주소 범위를 가집니다.
예를 들어 '10.0.1.0/24' 와 같은 형태의 CIDR 블록이 서브넷의 IP 주소 범위를 정의합니다.
이 CIDR 블록에 따라 서브넷 내에서 사용 가능한 IP 주소의 범위가 결정됩니다.
🎯 서브넷을 잘 설계하고 구성하는 것은 AWS에서 네트워크 인프라를 최적화하고 보안을 강화하는 데 매우 중요합니다.
-
-
-
-
-
-
🌐[Network] OSI 7계층 모델.
🌐[Network] OSI 7계층 모델.
다수의 시스템을 서로 연결해서 통신하려면 선행적으로 전체 시스템 구조를 표준화해야 합니다.
국제 표준화 단체인 ISO(International Standard Organization)에서는 OSI(Open Systems Interconnection) 7계층 모델을 제안하여, 네트워크에 연결된 시스템이 갖추어야 할 기본 구조와 기능을 상세히 정의하고 있습니다.
1️⃣ 계층 구조.
OSI 7계층 모델(OSI 7 Layer Model)에 따르면, 네트워크에 연결된 호스트들은 위 그림과 같이 7개 계층으로 모듈화된 전송 기능을 갖추어야 합니다.
일반 사용자는 OSI 7계층 맨 위에 있는 응용 계층을 통해 데이터의 송수신을 요청하며, 이 요청은 하위 계층에 순차적으로 전달되어 맨 아래에 있는 물리 계층을 통해 상대 호스트에 전송됩니다.
그리고 요청이 각 계층으로 하달되는 과정에서 송수신 호스트 사이의 라우터들이 중개 기능을 수행합니다.
일반적으로 라우터는 하위 3개 계층의 기능만 수행합니다.
데이터를 수신하는 호스트에서는 송신 호스트와는 반대 방향으로 처리가 이루어집니다.
즉, 물리 계층으로 들어온 데이터는 순차적인 상향 전달 과정을 거쳐 응용 계층으로 올라갑니다.
수신 호스트에서 처리가 완료된 결과를 회신할 때는 반대 과정을 순차적으로 밟아서 송신 호스트로 되돌아갑니다.
데이터를 송수신하는 최종 주체는 송수신 호스트 양쪽에 위치한 응용 계층이며, 하부 계층인 표현 계층은 응용 계층을 지원하기 위한 고유 기능을 수행합니다.
이와 같은 계층 구조의 원리는 모든 상하 계층에 대하여 상대적으로 적용되며, 각각의 계층들은 데이터 전송에 필요한 기능들을 나누어 처리합니다.
1️⃣ 계층별 기능.
OSI 7계층 모델의 각 계층은 독립적인 고유 기능을 수행하며, 하위 계층이 바로 위 계층에서 서비스를 제공하는 형식으로 동작합니다.
1️⃣ 물리 계층(Physical Layer).
네트워크에서 호스트들이 데이터를 전송하려면 반드시 물리적인 전송 매체로 연결되어 있어야 합니다.
물리 계층(Physical Layer)은 호스트를 전송 매체와 연결하기 위한 인터페이스 규칙과 전송 매체의 특성을 다루며, 크게 유선 매체와 무선 매체로 구분됩니다.
2️⃣ 데이터 링크 계층(Data Link Layer).
물리 계층으로 데이터를 전송하는 과정에서는 잡음(Noise)등과 같은 여러 외부 요인에 의하여 물리적인 오류가 발생할 수 있습니다.
데이터 링크 계층(Data Link Layer)은 물리 계층의 오류에 관한 오류 제어(Error Control) 기능을 수행하며, 이를 위해서는 오류의 발생 사실을 인지하는 기능과 오류 복구 기능이 필요합니다.
물리 계층은 물리적 전송 오류를 감지(Sense)하는 기능을 제공해 상위 계층인 데이터 링크 계층에서 오류를 인지할 수 있도록 해줍니다.
그렇지 않은 경우는 데이터 링크 계층 스스로 별도의 기능을 수행하여 오류를 인지해야 합니다.
대표적인 물리적 오류로는 데이터가 도착하지 못하는 데이터 분실과 내용이 깨져서 도착하는 데이터 변형이 있습니다.
일반적으로 컴퓨터 네트워크에서 오류 복구는 송신자가 원래의 데이터를 재전송(Retransmission)하는 방식으로 처리합니다.
3️⃣ 네트워크 계층(Network Layer)
송신 호스트가 전송한 데이터가 수신 호스트까지 안전하게 도착하려면 여러 개의 중개 시스템인 라우터(Router)를 거쳐야 합니다.
이 과정에서 데이터가 올바른 경로를 선택할 수 있도록 지원하는 계층이 네트워크 계층(Network Layer)입니다.
기본적으로 네트워크 내부 구조는 라우터들로 구성되고, 네트워크 바깥쪽에 연결되는 송수신 호스트 사이의 데이터 중개 기능을 수행합니다.
데이터 중개 과정에서 오류가 발생할 수 있으므로 네트워크 계층에도 오류 제어 기능이 필요합니다.
네트워크 부하가 증가하면 특정 지역에 혼잡(Congestion)이 발생할 수 있는데, 혼잡 제어(Congestion Control)도 데이터의 전송 경로와 관계되므로 네트워크 계층이 담당합니다.
4️⃣ 전송 계층(Transport Layer)
컴퓨터 네트워크에서 데이터를 교환하는 최종 주체는 호스트가 아니고, 호스트 내부에서 실행되는 응용 네트워크 프로세스입니다.
네트워크 계층은 송수신 호스트 사이의 전송을 지원하지만, 응용 프로세스까지 전달하는 기능은 없습니다.
전송 계층(Transport Layer)은 송신 프로세스와 수신 프로세스 간의 연결(Connection) 기능을 제공하기 때문에 프로세스 사이의 안전한 데이터 전송을 지원합니다.
전송 계층은 데이터가 전송되는 최종적인 경로상의 양 끝단 사이의 연결이 완성되는 계층입니다.
일반적으로 계층 4까지의 기능은 운영체제에서 시스템 콜(System Call) 형태로 상위 계층에 제공하며, 계층 5~7의 기능은 응용 프로그램으로 작성됩니다.
5️⃣ 세션 계층(Session Layer)
세션 계층(Session Layer)은 전송 계층에서 제공하는 연결의 개념과 유사한 세션 연결을 지원하지만, 이보다는 더 상위의 논리적 연결입니다.
즉, 응용 환경에서 사용자 간 대화(Dialog) 개념의 연결로 사용되기 때문에 전송 계층의 연결과 구분됩니다.
예를 즐어, 인터넷에서 파일 송수신 중에 연결이 끊기면 이는 전송 계층의 연결이 종료된 것입니다.
이후 전송 계층의 연결을 다시 설정하여 이전에 데이터 송수신이 멈춘 지점부터 이어서 전송하는 기능을 세션 계층이 지원합니다.
6️⃣ 표현 계층(Presentation Layer)
표현 계층(Presentation Layer)은 전송되는 데이터의 의미(Semantic)를 잃지 않도록 올바르게 표현(Syntax)하는 방법을 다룹니다.
즉, 정보를 교환하는 호스트들이 표준화된 방법으로 데이터를 인식할 수 있게 해줍니다.
또한, 데이터의 표현이라는 본래의 기능에 더해, 현재의 표현 계층은 압축과 암호화라는 기능도 중요하게 다루고 있습니다.
동영상과 같은 대용량의 멀티미디어 데이터를 압축(Compression)하면 전송 데이터의 양을 줄일 수 있습니다.
암호화는 네트워크 보안 기능의 하나이며, 외부의 침입자로부터 데이터를 안전하게 보호하는 기술입니다.
인터넷을 통한 개인 정보의 처리와 금융 상거래가 증가하면서 인터넷 보안의 중요성이 커지고 있습니다.
7️⃣ 응용 계층(Application Layer)
응용 계층(Application Layer)은 일반 사용자를 위한 다양한 네트워크 응용 서비스를 지원합니다.
단순히 정보 검색을 지원하던 시대를 지나서 오늘날 인터넷 환경은 인공지능과 결합하는 추세로 발전되고 있습니다.
그에 따라 특정 분야에 한정되지 않고, 사회 전반의 모든 영역으로 네트워크 서비스는 발전하고 있습니다.
-
-
-
🌐[Network] 시스템 기초 용어.
🌐[Network] 시스템 기초 용어.
위 그림과 같이 네트워크는 외형적으로 시스템과 전송 매체의 조합으로 구성됩니다.
데이터 통신을 위한 전송 매체는 전송 대역, 전송 속도, 전송 오류율과 같은 물리적인 특성이 주 관심사이므로 논리적인 기능은 비교적 단순합니다.
시스템은 전송 매체를 이용해 다양한 연동 형태로 구성할 수 있으므로 개념의 폭이 넓고 복잡합니다.
1️⃣ 시스템의 구분.
네트워크를 구성하는 시스템이 반드시 일반 컴퓨터처럼 복잡한 기능을 수행해야 하는 것은 아니지만, 데이터 전송 기능을 포함하여 일정 정도의 컴퓨팅 기능을 보유합니다.
네트워크 시스템은 수행 기능에 따라 다음과 같이 다양한 명칭으로 부를 수 있습니다.
노드, 라우터, 호스트, 클라이언트, 서버
1️⃣ 노드(Node)
노드(Node)는 컴퓨터 이론 분야에서 특정 시스템을 가리키는 가장 일반적인 용어로 사용됩니다.
인터넷에서도 상호 연결된 시스템을 표현할 수 있는 가장 포괄적 의미로 사용되므로 데이터를 주고받을 수 있는 모든 시스템을 통칭합니다.
노드는 인터넷 내부를 구성하는 라우터와 인터넷 바깥쪽에 연결되어 데이터를 주고받는 호스트로 구분됩니다.
2️⃣ 라우터(Router)
라우터(Router)는 인터넷 내부를 구성하며, 기본으로 데이터 전송 기능을 포함합니다.
라우터의 주요 역할은 데이터 중개 기능이며, 인터넷 바깥쪽에 연결된 호스트들 사이의 데이터 전송이 인터넷 내부에서 최적의 경로를 통하여 이루어지도록 합니다.
3️⃣ 호스트(Host)
호스트(Host)는 인터넷 바깥쪽에 연결되어 일반 사용자들의 네트워크 접속 창구 역할을 합니다.
일반적인 컴퓨팅 기능을 갖춘 호스트는 네트워크 응용 프로그램을 실행할 수 있고, 사용자는 이 프로그램을 이용하여 다양한 인터넷 서비스를 제공받습니다.
호스트는 로스트 사이에 제공되는 서비스를 기준으로 클라이언트와 서버로 나눌 수 있습니다.
4️⃣ 클라이언트(Client)와 서버(Server)
클라이언트(Client)는 임의의 인터넷 서비스를 이용하는 응용 프로그램이고, 서버(Server)는 서비스를 제공하는 응용 프로그램입니다.
클라이언트와 서버의 개념은 서비스 단위로 이루어지므로 임의의 호스트가 클라이언트나 서버로 고정되지 않습니다.
이용하는 서비스의 종류에 따라서 클라이언트가 될 수도 있고, 서버가 될 수도 있습니다.
그러므로 특정 서비스를 기준으로 상대적인 관점에서 클라이언트와 서버라는 용어를 사용합니다.
일반적으로 응용 프로그램 혹은 서비스 단위가 아닌 호스트 단위로도 클라이언트와 서버를 사용하기도 합니다.
즉, 다양한 서비스를 제공하는 목적으로 특화된 호스트의 경우 호스트 자체를 서버라 부르기도 합니다.
서버는 클라이언트보다 먼저 실행 상태가 되어 클라이언트의 요청에 대기해야 합니다.
그리고 영원히 종료하지 않으면서 클라이언트의 요청이 있을 때마다 서비스를 반복해서 제공합니다.
2️⃣ 클라이언트(Client)와 서버(Server)
위 그림은 임의의 응용 서비스를 기준으로 클라이언트와 서버의 상대적인 관계를 설명합니다.
FTP(File Transfer Protocol)는 원격 호스트끼리 파일 송수신 기능을 제공하는 서비스이고, 텔넷(Telnet)은 원격 호스트에 로그인하는 서비스를 제공합니다.
호스트 2는 FTP 서비스를 제공하고, 호스트 3은 텔넷 서비스를 제공합니다.
먼저, FTP 서비스를 살펴보면 호스트 1은 호스트 2에 FTP 서비스를 요청합니다.
따라서 FTP 서비스를 기준으로 하면 호스트 1이 클라이언트가 되고, 호스트 2는 서버가 됩니다.
반면, 텔넷 서비스는 호스트 2가 호스트 3에 서비스를 요청합니다.
텔넷 서비스를 기준으로 하면 호스트 2가 클라이언트이고, 호스트 3은 서버입니다.
따라서 호스트 2는 사용하는 응용 서비스의 종류에 따라 클라이언트가 되기도 하고 서버가 되기도 합니다.
결론적으로 클라이언트와 서버라는 용어는 서비스 이용의 상대적 위치에 따라 결정됨을 알 수 있습니다.
서버의 명칭을 특정 호스트에 전용으로 부여해서 사용할 수도 있습니다.
특히 다양한 서비스 기능을 제공하는 대형 시스템을 서버로 설정해 다수의 클라이언트가 접속해서 서비스를 이용하도록 할 수 있습니다.
그러나 기능적인 관점에서는 위 그림에서처럼 호스트에서 실행되는 응용 서비스별로 구분하는 것이 더 정확합니다.
인터넷에서 네트워크 서비스의 기능은 대부분 응용 프로그램으로 구현되므로 보통 클라이언트 프로세스, 서버 프로세스라는 호칭이 더 자연스러울 수 있습니다.
-
-
-
-
-
-
-
-
🌐[Network] 네트워크 기초 용어.
🌐[Network] 네트워크 기초 용어.
이미 수많은 사람이 익숙하게 사용하고 있는 인터넷(Internet)은 연구소, 기업, 학교 등의 소규모 조직에서 사용하기 시작한 작은 단위의 네트워크(Network)들을 서로 연결하면서 발전하였습니다.
그 과정에서 자연스럽게 연결 방식의 표준화를 요구하게 되었고, 오늘날 전 세계로 확산되어 거대한 인터넷으로 성장하였습니다.
네트워크를 이해하려면 시스템, 인터페이스, 전송 매체, 프로토콜, 네트워크, 인터넷과 같은 용어를 먼저 이해해야 합니다.
네트워크(Network)는 하드웨어적인 전송 매체(Transmission Media)를 매개로 서로 연결되어 데이터를 교환하는 시스템(System)의 모음이며, 시스템과 전송 매체의 연결 지점에 대한 규격이 인터페이스(Interface)입니다.
시스템이 데이터를 교환할 때는 소프트웨어적으로 동작하는 통신 규칙인 프로토콜(Protocol)이 필요합니다.
인터페이스와 프로토콜은 서로 다른 시스템을 상호 연동해 동작시키기 위함이니 반드시 연동 형식의 통일이 필요하고, 이를 표준화(Standardization)라 합니다.
위 그림은 여러 시스템이 전송 매체로 연결되어 네트워크를 구성한 예입니다.
시스템은 반드시 일반 컴퓨터일 필요는 없으며, 보통 컴퓨팅 기능을 보유한 네트워크 장비들을 의미합니다.
그림과 같은 네트워크의 가장 바깥쪽에 스마트폰을 포함한 일반 사용자들의 컴퓨터가 연결되어 데이터 교환 작업을 수행합니다.
시스템들은 물리적으로 공유하는 전송 매체에 의하여 서로 연결되지만, 시스템이 전송 매체를 통해 데이터를 교환하려면 반드시 표준화된 프로토콜을 사용해야 합니다.
우리가 알고 있는 인터넷은 IP(Internet Protocol)라는 네트워크 프로토콜이 핵심적인 역할을 하는 네트워크의 집합체입니다.
여기서 IP는 프로토콜의 의미가 포함된 약자이지만 보통 IP 프로토콜이라 부릅니다.
1️⃣ 시스템(System)
내부 규칙에 따라 자율적으로 동작하는 대상을 가리킵니다.
자동차, 커피 자판기, 컴퓨터, 마이크로프로세서, 하드디스크 등과 같은 물리적인 대상뿐 아니라, 신호등으로 교통을 제어하는 운영 시스템, Mac OS 등의 운영체제, 프로그램의 실행 상태를 의미하는 프로세스와 같은 소프트웨어적인 대상들도 시스템입니다.
🤩 TIP: 네트워크 환경에서 동작하는 임의의 시스템은 다른 시스템과 데이터를 교환하는 기능이 필수적입니다.
시스템의 동작에 필요한 외부 입력이 있을 수 있으며, 내부 정보와 외부 입력의 조합에 따른 출력(시스템 실행의 결과물)이 있을 수 있습니다.
한편, 작은 시스템이 여러 개 모여 더 큰 시스템을 구성할 수 있으므로 크기를 기준으로 시스템을 나누지는 않습니다.
우리가 알고 있는 인터넷은 수많은 소규모 네트워크들이 서로 연동되는 반복적인 과정을 거쳐서 형성된 거대 연합체의 네트워크를 의미합니다.
2️⃣ 인터페이스(Interface)
시스템과 시스템을 연결하기 위한 표준화된 접촉 지점을 의미하며, 하드웨어적인 관점과 소프트웨어적인 관점이 모두 존재합니다.
하드웨어적인 예로서, 컴퓨터 본체와 키보드를 연결하여 제대로 동작하게 하려면 키보드의 잭을 본체의 정해진 위치에 꽂아야 합니다.
이렇게 하려면 상호 간의 데이터 교환을 위한 RS-232C, USB 등과 같은 논리적인 규격뿐만 아니라, 잭의 크기와 모양 같은 물리적인 규격도 표준화되어야 합니다.
소프트웨어적인 예로서, 프로그래밍 언어에서 함수 설계자는 함수 이름과 매개변수를 표준화하여 정의해야 하고, 함수 사용자는 이 정의에 맞게 함수 이름과 인수를 지정하여 사용할 수 있습니다.
인터페이스를 논리적인 상하 구조의 개념으로 이해할 필요는 없지만, 양방향으로 데이터를 주고 받는 경우와 한쪽에서 다른 쪽의 단방향으로 데이터를 보내는 경우로 나눌 수 있습니다.
3️⃣ 전송 매체(Transmission Media)
시스템끼리 정해진 인터페이스를 연동해 데이터를 전달하려면 물리적인 전송 수단인 전송 매체(Transmission Media)가 반드시 있어야 합니다.
전송 매체는 사람의 눈으로 볼 수 있는 동축 케이블을 포함하여 소리를 전파하는 공기, 무선 신호 등 다양하게 존재합니다.
인터페이스는 시스템 간의 물리적인 연동을 위한 논리적인 규격이고 인터페이스로 정해진 규격은 전송 매체를 통해 물리적으로 구현되며, 시스템끼리 데이터 전송을 가능하게 합니다.
4️⃣ 프로토콜(Protocol)
논리적으로 상호 연동되는 시스템이 전송 매체를 통해 데이터를 교환할 때는 표준화된 대화 규칙을 따르는데, 이 규칙을 프로토콜(Protocol)이라 합니다.
일반적으로 프로토콜은 상하 관계가 아닌 동등한 위치에 있는 시스템 사이의 규칙이라는 측면이 강조되어 인터페이스와 구분이 됩니다.
인터페이스는 위 그림과 같이 두 시스템이 연동하기 위한 특정한 접촉 지점(Access Point)을 의미하는 경우가 많지만, 프로토콜과 비교하여 인용될 때는 상하 개념이 적용됩니다.
즉, 네트워크의 계층 모델 구조에서 인터페이스는 상하 계층 사이의 관계를 다루고, 프로토콜은 동등 계층 사이의 관계를 다룹니다.
일반적으로 프로토콜은 주고받는 데이터의 형식과 그 과정에서 발생하는 일련의 절차적 순서에 무게를 둡니다.
5️⃣ 네트워크(Network)
통신용 전송 매체로 연결된 여러 시스템이 프로토콜을 사용하여 데이터를 주고받을 때, 이들을 하나의 단위로 통칭하여 네트워크(Network)라 부릅니다.
일반적인 컴퓨터 네트워크에서는 물리적인 전송 매체로 연결된 컴퓨터들이 동일한 프로토콜을 이용해 서로 데이터를 주고 받습니다.
소규모 네트워크가 모여 더 큰 네트워크를 구성할 수 있는데, 네트워크끼리는 라우터(Router)라는 중개 장비를 사용해서 연결합니다.
6️⃣ 인터넷(Internet)
전 세계의 모든 네트워크가 유기적으로 연결되어 동작하는 통합 네트워크입니다.
인터넷에서 사용되는 시스템, 인터페이스, 전송 매체, 프로토콜들은 그 종류가 매우 복잡하고 다양하지만, 데이터 전달 기능에 한해서는 공통으로 IP(Internet Protocol) 프로토콜을 사용합니다.
즉, ISO의 OSI 7계층 모델에서 계층 3인 네트워크 계층의 기능을 IP 프로토콜이 수행하며 인터넷이라는 용어의 IP의 첫 단어인 Internet에서 유래했습니다.
7️⃣ 표준화(Standardization)
서로 다른 시스템이 상호 연동해 동작하려면 표준화(Standardization)라는 연동 형식의 통일이 필요합니다.
예를 들어, 프린트 용지를 생각해봅시다.
일반적으로 프린터와 프린트 용지를 만드는 회사는 다릅니다.
하지만 사전에 A4 규격이라는 통일된 틀을 만들어두었기 때문에 서로 다른 회사에서 생산한 프린터와 프린트 용지를 자유롭게 사용할 수 있습니다.
현대 산업사회가 눈부시게 성장한 배경에는 증기기관의 개발에 따른 에너지 동력원의 발전이 있었습니다.
지금은 인간의 노동력이라는 한계를 넘어 인공지능으로 대표되는 새로운 차원의 사회 발전 단계인 4차 산업혁명이 진행되고 있습니다.
그러나 이와 다른 관점에서 더 근원적인 발전 배경을 살펴보면, 표준화 원리를 바탕으로 한 레고의 조합 개념이 산업 전반에 존재해왔기 때문임을 알 수 있습니다.
-
-
-
☁️[AWS] 인바운드 규칙(Inbounds Rules)와 아웃바운드 규칙(Outbound Rules)
☁️[AWS] 인바운드 규칙(Inbounds Rules)와 아웃바운드 규칙(Outbound Rules).
AWS EC2 인스턴스의 보안 그룹은 인스턴스에 대한 네트워크 트래픽을 제어하는 가상 방화벽 역할을 합니다.
보안 그룹에는 인바운드(들어오는 트래픽) 및 아웃바운드(나가는 트래픽) 규칙이 있으며, 각 규칙은 특정 유형의 트래픽을 허용하거나 차단할 수 있습니다.
1️⃣ 인바운드 규칙(Inbound Rules)
인바운드 규칙은 외부에서 인스턴스로 들어오는 트래픽을 제어합니다.
이 규칙에 따라 특정 IP 주소나 IP 범위에서 오는 트래픽만 허용됩니다.
예시
SSH 접속을 허용하기 위해 포트 22번에서 들어오는 트래픽을 허용할 수 있습니다.
이 경우, 특정 IP 주소(예: 203.0.113.0/24)에서 SSH 접속이 가능하도록 설정할 수 있습니다.
웹 서버를 운영 중이라면 HHTP(포트 80) 또는 HTTPS(포트 443) 트래픽을 허용할 수 있습니다.
중요한 점
보안 그룹은 허용 규칙만 존재하며, 명시적으로 허용된 트래픽만 인스턴스로 들어올 수 있습니다.
기본적으로, 보안 그룹에 명시되지 않은 모든 인바운드 트래픽은 차단됩니다.
2️⃣ 아웃바운드 규칙(Outbound Rules)
아웃바운드 규칙은 인스턴스에서 외부로 나가는 트래픽을 제어합니다.
기본적으로 모든 아웃바운드 트래픽이 허용되지만, 필요에 따라 이를 제한할 수 있습니다.
예시
인스턴스가 특정 외부 서비스로의 연결을 허용하도록, 해당 서비스의 IP 주소나 포트로 나가는 트래픽을 허용할 수 있습니다.
만약 인스턴스가 외부로 데이터를 보내는 것을 제한하고자 한다면, 특정 포트나 IP 주소로의 나가는 트래픽을 차단할 수 있습니다.
중요한 점
기본적으로 아웃바운드 트래픽은 모두 허용되지만, 아웃바운드 규칙을 설정하여 특정 트래픽만 허용하도록 제한할 수 있습니다.
3️⃣ 보안 그룹 작동 방식
보안 그룹은 상태 정보를 가지고 있습니다. 즉, 인스턴스로 들어오는 요청이 허용되었다면, 그 요청에 대한 응답은 아웃바운드 규칙과 관계없이 허용됩니다.
보안 그룹은 AWS 계정 수준에서 관리되며, 여러 인스턴스에 동일한 보안 그룹을 적용할 수 있습니다.
보안 그룹의 변경 사항은 즉시 적용되므로, 보안 그룹을 수정하면 해당 인스턴스에 바로 반영됩니다.
🙋♂️ 마무리
보안 그룹을 올바르게 설정하는 것은 EC2 인스턴스를 안전하게 운영하기 위해 매우 중요합니다.
인바운드 규칙을 통해 접근을 제한하고, 필요에 따라 아웃바운드 규칙을 설정하여 인스턴스의 네트워크 트래픽을 제어할 수 있습니다.
-
-
☁️[AWS] 서비스 제공 형태에 따른 클라우드 분류.
☁️[AWS] 서비스 제공 형태에 따른 클라우드 분류.
클라우드 서비스는 제공하는 서비스에 따라 SasS, PaaS, IaaS 로 나눌 수 있습니다.
SaaS(Software as a Service)
응용 프로그램을 서비스로 제공하는 형태입니다.
많은 사람이 사용하는 Gmail, Dropbox, Office365, Zoom이 대표적인 SaaS 입니다.
PaaS(Platform as a Service), IaaS(Infrastructure as a Service)
응용 프로그램을 만들기 위한 기능을 서비스로 제공합니다.
이 서비스는 직접 응용 프로그램을 개발하는 사용자를 위한 서비스로, 사용자는 제공 받은 기능을 조합해 응용 프로그램을 개발합니다.
PaaS와 IaaS의 차이는 클라우드 서비스 제공자가 관리하는 범위입니다.
PaaS
클라우드 서비스 제공자는 OS 및 미들웨어까지 관리하고, 필수 기능만 사용자에게 제공합니다.
AWS에서 관리형 서비스로 제공하는 RDS나 DynamoDB, Lambda 등이 여기에 해당합니다.
유지보수는 AWS가 담당하며 사용자는 AWS에서 제공하는 범위 안에서 자유롭게 기능을 이용할 수 있습니다.
IaaS
서버 및 네트워크 기능만 제공하며 설정과 관리는 사용자의 몫입니다.
AWS의 EC2와 VPC, EBS와 같이 사용자가 자유롭게 설정할 수 있는 서비스가 IaaS에 해당합니다.
-
-
-
🌐[Network] CIDR이란?
🌐[Network] CIDR이란?
CIDR(Classless Inter-Domain Routing) 은 IP 주소와 관련된 라우팅 방법을 정의하는 표기법입니다.
CIDR 표기법은 IPv4 주소를 네트워크와 호스트 부분으로 나누고, 네트워크의 크기(서브넷 크기)를 정의하는 데 사용됩니다.
CIDR 표기법은 다음과 같은 형식으로 표현됩니다.
192.168.0.0/24
이 표기법은 두 부분으로 나뉩니다.
IP 주소 부분 : 192.168.0.0
서브넷 마스크 부분: /24
여기서 /24 는 서브넷 마스크의 길이를 나타내며, 이는 네트워크 부분의 비트 수를 의미합니다.
즉, 192.168.0.0/24 는 24비트가 네트워크를 정의하고 나머지 8비트(총 32비트 중)가 호스트를 정의하는 서브넷을 나타냅니다.
1️⃣ IPv4 CIDR의 구조.
IPv4 주소는 32비트로 구성되어 있으며, 이를 네 개의 8비트 옥텟으로 표현합니다.
예를 들어 다음과 같습니다.
11000000.10101000.00000000.00000000 (이진)
192.168.0.0 (십진)
CIDR 표기법에서 /24 는 첫 번째 24비트(세 개의 옥텟)가 네트워크 주소를 나타낸다는 것을 의미합니다.
이 경우 192.168.0.0 네트워크에는 192.168.0.1 에서 192.168.0.254 까지의 호스트 주소를 가질 수 있습니다.
2️⃣ IPv4 CIDR의 용도.
서브네팅 : 큰 네트워크를 작은 서브넷으로 나누기 위해 CIDR을 사용합니다.
라우팅 : 인터넷 서비스 제공자(ISP) 및 네트워크 관리자는 CIDR을 사용하여 라우팅 테이블을 관리하고, IP 주소 공간을 효율적으로 사용합니다.
IP 주소 관리 : CIDR은 IP 주소를 할당하고 네트워크를 관리하는 데 사용됩니다.
3️⃣ CIDR 블록의 생성 기준.
CIDR 블록을 생성할 때는 네트워크 크기와 필요한 IP 주소 수를 고려해야 합니다. 일반적인 기준은 다음과 같습니다.
1. 네트워크 크기 계산 :
/24 서브넷은 256개의 IP 주소(호스트)를 제공합니다. 이 중 두개의 주소(네트워크 주소와 브로드캐스트 주소)를 제외하고, 254개의 호스트 IP 주소를 사용할 수 있습니다.
/16 서브넷은 65,536개의 IP 주소를 사용할 수 있습니다.
/32 는 단일 IP 주소를 나타냅니다.
2. 필요한 IP 주소 수에 따라 결정 :
만약 50개의 장치를 연결해야 한다면, /26(64개 IP 주소 제공) 서브넷을 사용할 수 있습니다.
큰 네트워크에는 /16 이나 /12 처럼 더 작은 서브넷 마스크를 사용할 수 있습니다.
3. 보안 및 관리 :
더 작은 서브넷(CIDR 블록)을 사용하면 네트워크 트래픽을 보다 효율적으로 관리하고, 보안을 강화할 수 있습니다.
4️⃣ 예시.
/32 : 단일 IP 주소. 예: 192.168.0.1/32
/24 : 256개의 IP 주소 제공, 주로 작은 네트워크에서 사용. 예: 192.168.0.0/24
/16 : 65,536개의 IP 주소 제공, 더 큰 네트워크에 사용. 예: 192.168.0.0/16
/8 : 16,777,216개의 IP 주소 제공, 매우 큰 네트워크에서 사용. 예: 10.0.0.0/8
5️⃣ 결론.
CIDR 표기법은 IP 주소와 서브넷 마스크를 결합한 표준입니다.
네트워크의 크기와 IP 주소와 필요 수를 기준으로 CIDR 블록을 생성합니다.
CIDR을 사용하면 네트워크를 보다 효율적으로 관리하고 라우팅 테이블을 최적화할 수 있습니다.
🙋♂️ CIDR 블록을 설계할 때, 사용하려는 네트워크 규모와 IP 주소 요구 사항을 염두에 두고, 적절한 서브넷 마스크 길이를 선택하는 것이 중요합니다.
-
-
📝[Post] 정적 웹사이트와 동적 웹사이트.
🙋♂️ 정적 웹사이트와 동적 웹사이트.
정적 웹사이트와 동적 웹사이트는 웹페이지를 생성하고 제공하는 방식에서 큰 차이를 보입니다.
각각의 특징을 이해하면 어떤 상황에서 어떤 타입의 웹사이트를 사용해야 하는지 결정하는 데 도움이 됩니다.
1️⃣ 정적 웹사이트.
정적 웹사이트는 미리 만들어진 HTML 파일들을 그대로 웹 서버에서 사용자의 브라우저로 전송하여 보여주는 웹사이트입니다.
이 파일들은 서버에 미리 저장되어 있으며, 사용자의 요청에 따라 변하지 않고 그대로 제공됩니다.
👍 정적 웹사이트의 장점.
단순성과 속도.
복잡한 서버 측 처리 없이 바로 파일을 전송하기 때문에 로딩 시간이 빠릅니다.
호스팅 비용.
낮은 서버 자원 사용으로 인해 비용이 저렴합니다.
보안.
동적 콘텐츠를 처리하는 서버 측 스크립트가 없어 보안 리스크가 상대적으로 낮습니다.
👎 정적 웹사이트의 단점.
유연성 부족.
각 페이지를 수동으로 업데이트해야 하며, 대규모 사이트에서는 유지 관리가 어려울 수 있습니다.
사용자 상호작용 부족.
사용자 입력에 따라 내용이 바뀌지 않으므로, 폼 제출이나 검색과 같은 기능을 직접 구현하기 어렵습니다.
2️⃣ 정적 웹사이트의 예시.
1. 포트폴리오 웹사이트.
웹 개발자, 디자이너, 사진작가 등의 포트폴리오를 위한 웹사이트들은 주로 정적입니다.
이 웹사이트들은 작품을 보여주는 갤러리, 연락처 정보, 이력서 등의 고정된 내용을 포함합니다.
2. 기업 정보 페이지.
소규모 기업이나 스타트업이 회사 정보, 제품 설명, 연락처 정보 등을 제공하는 단순한 웹사이트를 운영할 때, 이는 종종 정적 웹사이트로 구성됩니다.
3. 이벤트 안내 페이지.
특정 이벤트의 일시, 장소, 등록 방법 등을 안내하는 웹페이지로, 주로 내용의 변경이 적고, 정보의 전달이 주 목적일 때 정적 웹사이트로 구현됩니다.
3️⃣ 동적 웹사이트.
동적 웹사이트는 서버 측 프로그래밍 언어를 사용하여 사용자의 요청에 따라 실시간으로 웹페이지를 생성하고 제공합니다.
데이터베이스와의 상호작용을 통해 컨텐츠를 동적으로 생성하고 사용자의 요청에 맞춰 개별적으로 내용을 조정할 수 있습니다.
👍 동적 웹사이트의 장점.
유연성.
사용자의 입력이나 상호작용에 따라 내용을 쉽게 변경할 수 있습니다.
기능성.
데이터베이스에 정보를 저장하고 검색하는 등의 복잡한 기능을 구현할 수 있습니다.
개인화.
사용자의 선호나 행동에 따라 개인화된 경험을 제공할 수 있습니다.
👎 동적 웹사이트의 단점.
비용과 복잡성.
서버 측 처리를 위한 추가적인 자원이 필요하며, 구현과 유지 관리가 복잡해질 수 있습니다.
보안 위험.
데이터베이스와 서버 측 스크립트를 사용함으로써 보안 취약점이 발생할 수 있습니다.
속도.
페이지를 실시간으로 생성하므로 처리 시간이 길어질 수 있습니다.
4️⃣ 동적 웹사이트의 예시.
1. 전자 상거래 플랫폼.
Amazon, eBay 등의 쇼핑 웹사이트는 사용자의 검색, 구매 이력, 상품의 재고 상태 등에 따라 실시간으로 정보를 업데이트하고 표시해야 합니다.
이런 기능은 동적 웹사이트 기술을 필요로 합니다.
2. 소셜 네트워킹 서비스.
Facebook, Twitter와 같은 소셜 미디어 플랫폼은 사용자의 상호 작용에 기반하여 내용이 계속 업데이트 되며, 이러한 동적 상호 작용을 지원합니다.
3. 온라인 교육 플랫폼.
Coursera, Udemy, Inflearn와 같은 교육 플랫폼은 사용자가 선택한 강좌에 따라 개인화된 학습 내용을 제공하고, 퀴즈 점수를 기록하며, 진행 상태를 추적합니다.
🙋♂️ 마무리
정적 웹사이트와 동적 웹사이트 선택은 프로젝트의 요구 사항, 예산, 기대하는 사용자 경험 등에 따라 달라집니다.
간단한 정보 제공 사이트의 경우 정적 웹사이트가 적합할 수 있고, 사용자 상호작용과 데이터 처리가 중요하 서비스는 동적 웹사이트가 더 적합할 수 있습니다.
이러한 예시들을 통해 정적 웹사이트가 주로 고정된 내용을 제공하는 반면, 동적 웹사이트는 사용자의 입력과 상호작용에 따라 콘텐츠가 변경되는 복잡한 기능을 필요로 함을 알 수 있습니다.
각각의 사례에서 요구하는 기능과 특성에 맞춰 웹사이트의 형태를 결정합니다.
-
-
-
☕️[Java] @RequiredArgsConstructor의 역할.
☕️[Java] @RequiredArgsConstructor 역할.
RequiredArgsConstructor 어노테이션은 Lombok 라이브러리에서 제공하는 기능 중 하나로, 클래스에 필수적인 생성자를 자동으로 생성하는 역할을 합니다.
이 어노테이션을 클래스에 적용하면, Lombok 이 그 클래스의 final 필드 또는 @NonNull 어노테이션이 붙은 필드를 인자로 받는 생성자를 자동으로 생성합니다.
1️⃣ @RequiredArgsConstructor의 주요 기능.
1. 자동 생성자 생성
클래스 내의 모든 final 필드와 @NonNull 어노테이션이 붙은 필드에 대한 생성자를 자동으로 생성합니다.
이 생성자는 이 필드들을 초기화하는 데 필요한 파라미터를 요구합니다.
2. 코드 간결화
수동으로 생성자를 작성하는 번거로움을 줄여줍니다.
특히 많은 필드를 가진 클래스에서 유용하게 사용될 수 있습니다.
3. 불변성 강화
final 필드를 사용함으로써 클래스의 불변성을 강화할 수 있습니다.
생성자를 통해 한 번 설정되면, 이 필드들의 값은 변경될 수 없습니다.
4. 의존성 주입 용이
Spring과 같은 프레임워크에서 생성자를 통한 의존성 주입을 사용할 때 유용합니다.
필요한 의존성을 생성자를 통해 주입받기 때문에, 컴포넌트 간의 결합도를 낮출 수 있습니다.
2️⃣ 사용 예시.
다음은 @RequiredArgsConstructor 어노테이션을 사용한 간단한 클래스 예제입니다.
import lombok.RequiredArgsConstructor;
import lombok.NonNull;
@RequiredArgsConstructor
public class UserData {
private final String username; // final 필드에 대한 생성자 파라미터 자동 포함.
@NonNull private String email; // @NonNull 필드에 대한 생성자 파라미터 자동 포함.
// 추가 메소드 등
}
위 코드에서 UserData 클래스에는 username 과 email 두 필드가 있으며, username 은 final 로 선언되어 수정할 수 없고, email 은 @NonNull 어노테이션이 붙어 null 값을 허용하지 않습니다.
Lombok은 이 두 필드를 초기화하는 생성자를 자동으로 생성합니다.
3️⃣ 주의 사항.
@RequiredArgsConstructor 는 필드가 많고, 특히 final 또는 @NonNull 필드가 있는 경우 유용합니다.
그러나 생성자를 통한 초기화가 필요하지 않은 필드에는 적용되지 않습니다.
Lombok을 사용하면 코드가 간결해지고 가독성이 향상되지만, 코드의 명시성이 다소 떨어질 수 있습니다.
따라서 Lombok 사용 시, 팀 내에서 Lombok에 대한 이해도가 충분한지 확인하는 것이 좋습니다.
Lombok 의 @RequiredArgsConstructor 는 반복적인 코드 작성을 줄여주고, 오류 가능성을 감소시키며, 더 깔끔하고 관리하기 쉬운 코드베이스를 유지하는 데 도움을 줄 수 있습니다.
-
-
☕️[Java] @Transactional의 역할과 의미.
☕️[Java] @Transactional의 역할과 의미.
@Transaction 어노테이션은 스프링 프레임워크에서 제공하는 선언적 트랜젝션 관리 기능을 활용하기 위해 사용됩니다.
이 어노테이션을 사용함으로써, 특정 메서드 또는 클래스 전체에 걸쳐 데이터베이스 트랜잭션의 경계를 설정할 수 있습니다.
트랜잭션은 일련의 연산들이 전부 성공적으로 완료되거나, 하나라도 실패할 경우 전체를 취소(롤백)하여 데이터의 일관성과 정합성을 보장하는 것을 목적으로 합니다.
1️⃣ @Transactional의 주요 기능과 특징.
1. 자동 롤백
@Transactional 이 적용된 메서드에서 런타임 예외(RuntimeException)가 발생하면, 그 트랜잭션에서 수행된 모든 변경이 자동으로 롤백됩니다.
이는 데이터의 일관성을 유지하는 데 필수적입니다.
2. 프로파게이션(Propagation)
트랜잭션의 전파 행위를 제어합니다.
예를 들어, 이미 진행 중인 트랜잭션이 있을 때 새로운 트랜잭션을 시작할 것인지, 아니면 기존 트랜잭션을 참여할 것인지 결정할 수 있습니다.
REQUIRED(기본값) : 이미 진행 중인 트랜잭션이 있다면 그 트랜잭션이 참여하고, 없다면 새로운 트랜잭션을 시작합니다.
REQUIRED_NEW : 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있다면 잠시 보류합니다.
3. 격리 수준(Isolation Level)
다른 트랜잭션이 데이터에 동시에 접근했을 때 발생할 수 있는 문제를 제어합니다.
예를 들어, READ_COMMITTEED, REPEATED_READ, SERIALIZABLE 등 다양한 격리 수준을 지정할 수 있습니다.
4. 읽기 전용(Read-Only)
트랜잭션을 읽기 전용으로 설정할 수 있어, 데이터 수정이 이루어지지 않는다는 것을 데이터베이스 최적화 엔진에 알려 성능을 향상시킬 수 있습니다.
5. 롤백 규칙(Rollback Rules)
특정 예외가 발생했을 때 롤백을 수행할지 아니면 커밋을 수행할지를 세밀하게 제어할 수 있습니다.
기본적으로 런타임 예외에서는 롤백을 수행하고, 체크 예외에서는 커밋을 수행합니다.
2️⃣ 사용 예제.
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
@Service
public class TransactionalService {
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id);
}
@Transactional(rollbackFor = Exception.class)
public User updateUser(User user) {
return userRepository.save(user);
}
}
위 예시처럼, getUser 메서드는 데이터를 변경하지 않고 조회만 수행하기 때문에 readOnly = true 로 설정했습니다.
반면, updateUser 메서드는 데이터를 변경할 가능성이 있으므로, 모든 예외(Exception)가 발생할 경우 롤백하도록 설정했습니다.
@Transactional 을 사용함으로써 개발자는 복잡한 트랜잭션 관리 코드를 직접 작성하지 않고도, 스프링 프레임워크가 제공하는 선언적 방식을 통해 간단하게 트랜잭션을 관리할 수 있게 됩니다.
이는 애플리케이션의 데이터 처리 로직을 더욱 안정적이고 효율적으로 만듭니다.
-
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화
☕️[Java] ObjectMapper 클래스, 직렬화와 역직렬화.
ObjectMapper 는 주로 JSON 데이터를 처리하기 위해 사용되는 Jackson 라이브러리의 핵심 클래스입니다.
이 클래스는 자바 객체와 JSON 형식 간의 직렬화(Serialization)와 역직렬화(Deserialization)를 수행합니다.
ObjectMapper 는 JSON 데이터를 자바 객체로 변환하거나 자바 객체를 JSON 데이터로 변환하는 등의 작업을 매우 효율적으로 처리할 수 있게 해줍니다.
1️⃣ 직렬화(Serialization)
ObjectMapper 를 사용하여 자바 객체를 JSON 문자열로 직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
pulbic class User {
public String name;
public int age;
}
// 직렬화 예제
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.name = "Kobe";
user.age = "30";
String json = mapper.writeValueAsString(user); // 자바 객체를 JSON 문자열로 변환
System.out.println(json);
2️⃣ 역직렬화(Deserialization)
ObjectMapper 를 사용하여 JSON 문자열을 자바 객체로 역직렬화하는 과정은 다음과 같습니다.
import com.fasterxml.jackson.databind.ObjectMapper;
// 예시 자바 객체
public class User {
public String name;
public int age;
}
// 역직렬화 예제
ObjectMapper mapper = new ObjectMapper();
String json = "{\"name\":\"Kobe\", \"age\":30}";
User user = mapper.readValue(json, User.class); // JSON 문자열을 자바 객체로 변환
Systeom.out.println(user.name + " is" + user.age + " year old.");
3️⃣ 주요 기능
다양한 데이터 포맷 지원 : ObjectMapper 는 JSON 외에도 XML, CSV 등 여러 데이터 포맷을 지원합니다.(Jackson 데이터 포맷 모듈 설치 필요).
유연성과 설정 : ObjectMapper 는 맞춤 설정이 가능하여, 다양한 JSON 직렬화/역직렬화 방법을 지원합니다.
예를 들어, 필드 이름의 자동 감지, 날짜 형식 지정, 무시할 필드 설정 등을 조정할 수 있습니다.
성능 : Jackson은 JSON 처리를 위해 최적화된 라이브러리 중 하나로, 대용량 데이터 처리에도 뛰어난 성능을 보입니다.
🤔 직렬화와 역직렬화란?
직렬화(Serialization)와 역직렬화(Deserialization)는 데이터 구조 또는 객체 상태를 저장하고 전송하기 위해 다루기 쉬운 데이터 포맷으로 변환하는 과정을 의미합니다.
컴퓨터 과학의 맥락에서 이 개념은 특히 중요하며, 객체 지향 프로그래밍에서 널리 사용됩니다.
1️⃣ 직렬화(Serialization)
직렬화는 객체의 상태(즉, 객체가 가진 데이터와 그 구조)를 일련의 바이트로 변환하는 과정입니다.
이 바이트 스트림은 나중에 파일, 데이터베이스 또는 네트워크를 통해 쉽게 저장하거나 전송할 수 있습니다.
예를 들어, 자바에서는 Serialization 인터페이스를 구현한 객체를 바이트 스트림으로 변환하여 파일 시스템에 저장하거나 네트워크를 통해 다른 시스템으로 보낼 수 있습니다.
2️⃣ 직렬화의 주요 목적.
영속성 : 객체의 상태를 영구적으로 저장하여 나중에 다시 로드할 수 있습니다.
네트워크 전송 : 객체를 네트워크를 통해 다른 시스템으로 전송하기 위해 사용됩니다.
데이터 교환 : 다양한 언어나 플랫폼 간의 데이터 교환이 가능하도록 합니다.
3️⃣ 역직렬화(Deserialization)
역직렬화는 직렬화된 바이트 스트림을 다시 원래의 객체 상태로 복원하는 과정입니다.
즉, 파일, 데이터베이스 또는 네트워크로부터 바이트 스트림을 읽어 들여서 실행 중인 프로그램에서 사용할 수 있는 실제 객체로 변환합니다.
이 과정은 직렬화의 반대 과정으로, 복원된 객체는 원복 객체와 동일한 상태를 가집니다.
4️⃣ 역직렬화의 주요 사용 사례.
객체 복원 : 저장되거나 전송된 데이터로부터 객체를 재구성합니다.
상태 복구 : 애플리케이션의 이전 상태를 복구하는 데 사용됩니다.
데이터 접근 : 다른 시스템에서 전송된 데이터를 로컬 시스템에서 접근하고 사용할 수 있게 합니다.
5️⃣ 데이터 포맷과 직렬화 도구
다양한 데이터 포맷(JSON, XML, YAML 등)과 여러 프로그래밍 언어 또는 라이브러리에서 직렬화와 역직렬화를 지원합니다.
자바에서는 ObjectMapper 를 사용해 JSON 데이터 포맷으로의 직렬화와 역직렬화를 처리하며, 이는 데이터를 쉽게 읽고 쓸 수 있는 구조로 만드는 데 유용합니다.
-
-
☕️[Java] attribute의 의미와 역할
☕️[Java] attribute의 의미와 역할.
Java 백엔드 개발에서 “attribute”라는 용어는 몇 가지 다른 맥락에서 사용될 수 있습니다.
주로 두 가지 의미로 사용되는 경우가 많은데, 클래스의 속성 을 의미하는 경우와 웹 개발에서 HTTP 요청이나 세션과 관련된 데이터를 지칭하는 경우입니다.
1️⃣ 클래스의 속성(Field or Property)
Java에서 클래스의 “attribute” 는 해당 클래스의 상태를 정의하는 변수를 말합니다.
이러한 변수들은 객체의 데이터이터를 저장하고, 클래스의 인스턴스들이 갖는 특징과 상태 정보를 나타냅니다.
예를 들어, ‘Person’ 클래스가 있다면, ‘name’, ‘age’ 같은 필드들이 이 클래스의 “attribute” 가 됩니다.
public class Person {
private String name; // Attribute
private int age; // Attribute
// Constructors, getters, setters 등
}
2️⃣ 웹 개발에서의 Attribute
웹 개발에서 “attribute” 는 주로 세션(Session)이나 요청(Request) 객체에 저장된 데이터를 지칭 합니다.
이 데이터는 사용자가 웹 사이트를 이용하는 동안 지속되거나 요청 동안에만 존재할 수 있습니다.
예를 들어, 사용자가 로그인을 하면 그 사용자의 정보를 세션 attribute로 저장하여 다른 페이지에서도 사용자 정보를 유지할 수 있게 합니다.
// 세션에 사용자 정보 저장
request.getSession().setAttribute("user", userObject);
// 세션에서 사용자 정보 가져오기
User user = (User) request.getSession().getAttribute("user");
이 두 가지 사용 사례는 Java 백엔드 개발에서 매우 흔하게 접할 수 있으며, 각각의 맥락에서 attribute가 가지는 의미와 역할을 이해하는 것은 중요합니다.
첫 번째 경우는 객체 지향 프로그래밍의 핵심 요소로 클래스의 속성을 정의합니다.
두 번째 경우에는 웹 애플리케이션의 상태 관리를 돕는 수단으로서 활용됩니다.
-
-
-
-
-
-
📝[Post] 자바다식(Java多識) - 2
자바다식(Java多識) 2편.
1. @AfterEach 어노테이션.
@AfterEach 어노테이션은 JUnit 5에서 제공하는 기능입니다.
각 테스트 메서드가 실행된 후에 수행되어야 하는 작업을 지정하는 데 사용됩니다.
이 어노테이션은 테스트 클래스 내의 메서드에 적용하여 테스트 메서드가 끄탄 후 필요한 정리 작업(cleanup)을 수행할 수 있도록 합니다.
주요 역할
1. 자원 해제 : 테스트 메서드가 사용한 자원(예: 파일, 데이터베이스 연결, 네트워크 연결 등)을 해제하는 데 사용됩니다.
2. 상태 초기화 : 테스트가 완료된 후 상태를 초기화하여 다음 테스트가 깨끗한 환경에서 실행될 수 있도록 합니다.
3. 로그 기록 : 테스트 실행 결과를 로그에 기록하거나 추가적인 분석을 위해 데이터를 저장하는 데 사용할 수 있습니다.
예제 코드
아래는 @AfterEach 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MyTest {
@BeforeEach
void setUp() {
System.out.println("Setting up before each test");
}
@Test
void testMethod1() {
System.out.println("Executing test method 1");
}
@Test
void testMethod2() {
Systemo.out.println("Executing test method 2");
}
@AfterEach
void tearDown() {
System.out.println("Tearing down after each test")
}
}
실행 순서
setUp() : 각 테스트 메서드 실행 전 @BeforeEach 메서드가 호출됩니다.
testMethod1() : 첫 번째 테스트 메서드가 실행됩니다.
tearDown() : 첫 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
setUp() : 두 번째 테스트 메서드 실행 전 @BeforeEach 메서드가 다시 호출됩니다.
testMethod2() : 두 번째 테스트 메서드가 실행됩니다.
tearDown() : 두 번째 테스트 메서드 실행 후 @AfterEach 메서드가 호출됩니다.
요약
@AfterEach 어노테이션은 각 테스트 메서드 실행 후 호출되는 메서드를 지정합니다.
주로 자원 해제, 상태 초기화, 로그 기록 등의 작업을 수행하는 데 사용됩니다.
각 테스트 메서드마다 실행되므로, 테스트 간의 독립성을 유지하고 깨끗한 테스트 환경을 보장할 수 있습니다.
2. @Builder 어노테이션.
@Builder 는 Lombok 라이브러리에서 제공하는 어노테이션으로, 빌더 패턴을 간편하게 사용할 수 있도록 지원합니다.
빌더 패턴은 객체의 생성과 관련된 복잡성을 줄이고, 가독성을 높이며, 가변 객체를 만들지 않도록 도와줍니다.
특히, 많은 필드를 가진 객체를 생성할 때 유용합니다.
주요 특징 및 역할.
유연한 객체 생성
빌더 패턴을 사용하면 객체를 생성할 때 생성자나 정적 팩토리 메서드보다 더 유연하게 객체를 구성할 수 있습니다.
필요한 필드만 설정할 수 있고, 설정 순서에 구애받지 않습니다.
가독성 향상
많은 필드를 가진 객체를 생성할 때, 빌더 패턴을 사용하면 코드의 가독성이 높아집니다.
각 필드의 이름을 명시적으로 설정할 수 있어 어떤 값이 어떤 필드에 설정되는지 쉽게 할 수 있습니다.
불변 객체 생성
빌더 패턴을 사용하면 불변 객체를 쉽게 생성할 수 있습니다.
객체가 생성된 후에는 필드 값을 변경할 수 없습니다.
사용 예시
Lombok 없이 빌더 패턴 구현
public class User {
private final String name;
private final int age;
private final String email;
private User(UserBuilder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
public static class UserBuilder {
private String name;
private int age;
private String email;
public UserBuilder setName(String name) {
this.name = name;
return this;
}
public UserBuilder setAge(int age) {
this.age = age;
return this;
}
public UserBuilder setEmail(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
}
Lombok을 사용한 빌더 패턴 구현
Lombok의 @Builder 어노테이션을 사용하면 위의 코드가 크게 단축됩니다.
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class User {
private String name;
private int age;
private String email;
}
객체 생성 예시
위의 Lombok을 사용한 User 클래스를 이용해 객체를 생성하는 예시입니다.
public class Main {
public static void main(String[] args) {
User user = User.builder()
.name("devKobe")
.age(77)
.email(devKobe@gamil.com)
.build();
System.out.println(user.getName()); // devKobe
System.out.println(user.getAge()); // 77
System.out.println(user.getEmail()) // devKobe@gmail.com
}
}
위 예시처럼 Lombok의 @Builder 를 사용하면 빌더 패턴을 간단하게 구현하고 사용할 수 있습니다.
이로 인해 객체 생성 코드가 더 깔끔하고 직관적으로 변합니다.
-
📝[Post] 자바다식(Java多識) - 1
자바다식(Java多識) 1편.
1. ‘mainClassName’ 속성 추가.
메인 클래스의 경로를 지정해주는 속성을 추가하는 방법입니다.
초기 진입점을 지정해준다고 생각하면 됩니다.
application 블록 안에 메인 클래스 이름을 지정합니다. 예를 들어, 메인 클래스가 com.example.Main 이라고 가정합니다.
아래의 코드는 bundle.gradle 파일 내부에서 수정해야 합니다.
plugins {
id 'java'
id 'application'
}
application {
mainClassName = 'com.example.Main' // 여기에 메인 클래스의 경로를 입력합니다.
applicationDefaultJvmArgs = [
"-XX:+EnableDynamicAgentLoading",
"-Djdk.instrument.traceUsage"
]
}
repositories {
mavenCentral()
}
dependencies {
// Your dependencies here
}
메인 클래스 예시
예를 들어, 메인 클래스는 다음과 같이 생겼을 수 있습니다.
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
2. @ExtendWith 어노테이션.
@ExtendWith 어노테이션은 Junit 5에서 제공하는 기능으로, 테스트 클래스나 메서드에 확장 기능을 추가할 수 있도록 해줍니다.
JUnit 5의 확장 모델은 다양한 확장 기능을 통해 테스트 실행의 특정 지점에서 사용자 정의 동작을 수행할 수 있게 합니다.
@ExtendWith 어노테이션의 역할
확장 클래스 지정 : @ExtendWith 어노테이션은 확장 클래스를 지정할 수 있습니다. 지정된 확장 클래스는 테스트 라이프사이클의 특정 지점에서 호출됩니다.
예를 들어, 테스트 실행 전후, 각 테스트 메서드 전후 등 다양한 시점에서 특정 동작을 추가할 수 있습니다.
컨텍스트 설정 및 주입 : 확장 기능을 통해 테스트 컨텍스트를 설정하고, 테스트 메서드에 필요한 객체나 리소스를 주입할 수 있습니다. 이를 통해 테스트 코드를 더 간결하고 모듈화할 수 있습니다.
조건부 실행 : 특정 조건에 따라 테스트 메서드를 실행하거나 건너뛸 수 있도록 지원합니다.
예를 들어, 특정 환경 설정이나 시스템 상태에 따라 테스트 실행 여부를 결정할 수 있습니다.
커스텀 어서션 및 보고 : 확장을 통해 사용자 정의 어서션 로직을 추가하거나 테스트 결과를 커스텀 방식으로 보고할 수 있습니다.
예제 코드
아래는 @ExtendWith 어노테이션을 사용한 간단한 예제입니다.
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.Test;
class MyExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
System.out.println("Before each test method");
}
}
@ExtendWith(MyExtension.class)
public class MyTest {
@Test
void testMethod1() {
System.out.println("Test method 1");
}
@Test
void testMethod2() {
System.out.println("Test method 2");
}
}
위 코드에서 MyExtension 클래스는 BeforeEachCallback 인터페이스를 구현하여 각 테스트 메서드가 실행되기 전에 메시지를 출력합니다.
@ExtendWith(MyExtension.class) 어노테이션을 통해 MyTest 클래스에 이 확장 기능을 추가했습니다.
따라서 각 테스트 메서드 실행 전에 “Before each test method” 메시지가 출력됩니다.
이처럼 @ExtendWith 어노테이션은 JUnit 5의 확장 모델을 활용하여 테스트에 필요한 다양한 기능을 추가할 수 있게 해줍니다.
3. 어서션(Assertion)
어서션(Assertion)은 프로그래밍 및 소프트웨어 테스트에서 코드의 특정 상태나 조건이 참인지 확인하는 데 사용되는 문장이나 명령문을 의미합니다.
어서션을 통해 코드의 논리적 일관성과 정확성을 검증할 수 있으며, 주로 디버깅과 테스트에 사용됩니다.
주요 기능과 목적.
조건 검증 : 어서션(Assertion)은 특정 조건이 참인지 검증합니다. 조건이 거짓이면 프로그램은 즉시 실행을 중단하고 오류를 보고합니다.
디버깅 도구 : 어서션(Assertion)은 개발 중에 코드의 오류를 조기에 발견하고 수정하는 데 도움이 됩니다. 코드의 가정이 잘못된 경우 어서션을 통해 문제를 빨리 찾을 수 있습니다.
문서화 : 어서션(Assertion)은 코드의 논리적 전제 조건을 명시적으로 표현하여, 코드가 어떤 상태에 작동해야 하는지 명확하게 나타냅니다.
어서션(Assertion)의 예
자바(Java)
public void setAge(int age) {
assert age > 0 : "Age must be positive";
this.age = age;
}
JUnit (Java)
```java
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class MyTest {
@Test
void testAddition() {
int result = 2 + 3;
assertEquals(5, result, "2 + 3 should equal 5");
} } ```
어서션 사용 시기
개발 중 : 개발자가 코드의 논리적 일관성을 검증하기 위해 사용합니다. 디버깅 과정에서 주로 사용되며, 프로덕션 환경에서는 보통 비활성화합니다.
테스트 코드 : 테스트 프레임워크(JUnit, TestNG 등)를 사용하여 테스트를 작성할 때, 특정 조건이 기대한 대로 동작하는지 확인합니다.
주의 사항
프로덕션 코드에서의 사용 : 어서션은 주로 개발 및 테스트 환경에서 사용되며, 프로덕션 환경에서는 비활성화되는 경우가 많습니다. 프로덕션 환경에서 조건 검증이 필요한 경우에는 예외 처리를 사용합니다.
부작용 없는 코드 : 어서션 내부에서는 부작용이 없는 코드를 사용하는 것이 좋습니다. 어서션은 상태를 변경하지 않고 조건만 검증해야 합니다.
요약.
어서션은 코드의 특정 조건이 참임을 검증하는 도구로, 디버깅과 테스트 과정에서 코드의 논리적 일관성을 유지하는데 중요한 역할을 합니다.
이를 통해 개발자는 코드의 가정과 실제 동작이 일치하는지 확인하고, 문제를 조기에 발견하여 수정할 수 있습니다.
-
☕️[Java] 프로그래밍 언어와 자바
변수 선언.
컴퓨터 메모리(RAM)은 수많은 번지들로 구성된 데이터 저장 공간입니다.
프로그램은 데이터를 메모리에 저장하고 읽는 작업을 비번히 수행합니다.
이때 데이터를 어디에, 어떤 방식으로 저장할지 정해져 있지 않다면 메모리 관리가 무척 어려워집니다.
이 문제를 해결하기 위해 변수(Variable)을 사용합니다.
변수(Variable)는 하나의 값을 저장할 수 있는 메모리 번지에 붙여진 이름입니다.
변수를 통해 프로그램은 메모리 번지에 값을 저장하고 읽을 수 있습니다.
자바의 변수는 다양한 타입의 값을 저장할 수 없습니다.
즉, 정수형 변수에는 정수값만 저장할 수 있고, 실수형 변수에는 실수값만 저장할 수 있습니다.
변수를 사용하려면 변수 선언이 필요합니다.
변수 선언은 어떤 타입의 데이터를 저장할 것인지 그리고 변수 이름이 무었인지 결정하는 것입니다.
int age; // 정수(int) 값을 저장할 수 있는 age 변수 선언
double value; // 실수(double) 값을 저장할 수 있는 value 변수 선언
변수 이름의 첫 번째 글자가 문자여야 하고, 중간부터는 문자, 숫자, $, _를 포함할 수 있습니다.
또한, 첫 문자를 소문자로 시작하되 캐멀 케이스로 작성하는 것이 관례입니다.
변수가 선언 되었다면 값을 저장할 수 있습니다.
이때 대입 연산자인 =를 사용합니다.
수학에서 등호(=)는 ‘같다’라는 의미이지만, 자바에서는 우측 값을 좌측 변수에 대입하는 연산자로 사용됩니다.
int score; // 변수 선언
score = 60; // 값 대입
변수 선언은 저장되는 값의 타입과 이름만 결정한 것이지, 아직 메모리에 할당된 것은 아닙니다.
변수에 최초로 값이 대입될 때 메모리에 할당되고, 해당 메모리에 값이 저장됩니다.
변수에 최초로 값을 대입하는 행위를 변수 초기화라고 하고, 이때의 값을 초기값이라고 합니다.
초기 값은 다음과 같이 변수를 선언함과 동시에 대입할 수도 있습니다.
int score = 90;
초기화되지 않은 변수는 아직 메모리에 할당되지 않았기 때문에 변수를 통해 메모리 값을 읽을 수 없습니다.
따라서 다음은 잘못된 코딩입니다.
int value; // <- 1.변수 value 선언
int result = value + 10; // <- 2.변수 value 값을 읽고 10을 더해서 변수 result에 저장
1 에서 변수 value가 선언되었지만, 초기화되지 않았기 때문엔 2 value + 10에서 value 변수값은 읽어올 수 없습니다.
따라서 위 코드는 다음과 같이 변경해야 합니다.
int value = 30; // 변수 value가 30으로 초기화됨
int result = value + 10; // 변수 value 값(30)을 읽고 10을 더해서 변수 result에 저장
다음 예제는 초기화되지 않은 변수를 연산식에 사용할 경우 컴파일 에러(The local variable value may not have been initializer)가 발생하는 것을 보여줍니다.
public class VariableInitializationExample {
public static void main(String[] args) {
// 변수 value 선언
int value;
// 연산 결과를 변수 result의 초기값으로 대입
int result = value + 10; // <------- 컴파일 오류
// 변수 result 값을 읽고 콘솔에 출력
System.out.println(result);
}
}
변수는 출력문이나 연산식에 사용되어 변수값을 활용합니다.
다음 예제는 변수를 문자열과 결합 후 출력하거나 연산식에서 활용하는 모습을 보여줍니다.
```java
public class VariableUseExample {
public static void main(String[] args) {
int hour = 3;
int minute = 5;
System.out.println(hour + “시간” + minute + “분”);
int totalMinute = (hour*60) + minute;
System.out.println("총" + totalMinute + "분"); } }
// 실행 결과
// 3시간 5분
// 총 185분
- 변수는 또 다른 변수에 대입되어 메모리 간에 값을 복사할 수 있습니다.
- 다음 코드는 변수 x 값을 변수 y 값으로 복사합니다.
```java
int x = 10; // 변수 x에 10을 대입
int y = x; // 변수 y에 변수 x값을 대입
다음 예제는 두 변수의 값을 교환하는 방법을 보여줍니다.
두 변수의 값을 교환하기 위해서 새로운 변수 temp를 선언한 것에 주목합시다.
```java
public class VariableExchangeExample {
public static void main(String[] args) {
int x = 3;
int y = 5;
System.out.println(“x:” + x + “, y:” + y);
int temp = x;
x = y;
y = temp;
System.out.println(“x:” + x + “, y:” + y);
}
}
// 실행 결과
// x:3, y:5
// x:5, y:3
```
-
💾[Database] 데이터베이스의 정의와 특징.
💾[Database] 데이터베이스의 정의와 특징.
1️⃣ 데이터베이스 : 여러 사용자나 응용 프로그램이 공유하고 동시에 접근 가능한 ‘데이터의 집합’ 이라고 정의할 수 있습니다.
2️⃣ DBMS(DataBase Management System) : ‘데이터베이스’를 ‘관리,운영하는 소프트웨어’ 입니다.
🙋♂️ 데이터베이스
‘데이터 저장 공간’ 자체를 의미하기도 합니다.
DBMS 중 하나인 MySQL에서는 ‘데이터베이스’를 ‘자료가 저장되는 디스크 공간(주로 파일로 구성됨)’으로 취급합니다.
위 그림은 데이터베이스, DBMS, 사용자, 응용 프로그램의 관계를 보여줍니다.
위 그림에서 보듯이 DBMS는 데이터베이스를 관리하는 역할을 하는 소프트웨어입니다.
여러 사용자나 응용 프로그램은 DBMS가 관리하는 데이터에 동시에 접속하여 데이터를 공유합니다.
👉 즉, DBMS에서는 데이터베이스에서 사용되는 데이터가 집중 관리됩니다.
🙋♂️ 데이터베이스와 DBMS
데이터베이스를 DBMS와 혼용해서 같은 용어처럼 사용하는 경우도 흔히 있습니다.
바라보는 시각에 따라 그렇게 사용하는 것이 틀린 것은 아니지만
저는 데이터베이스를 ‘데이터의 집합’ 또는 ‘데이터의 저장 공간’으로 보고,
DBMS는 데이터베이스를 운영하는 ‘소프트웨어’라는 의미로 공부하겠습니다.
DBMS에는 MySQL 외에도 많은 종류의 프로그램이 있습니다.
MySQL
MariaDB
PostgreSQL
Oracle
SQL Server
DB2
Access
SQLite
…
🙋♂️ 위 명시된 리스트는 2018년 기준 많이 사용되는 DBMS입니다.
3️⃣ DBMS 또는 데이터베이스의 몇 가지 중요한 특징.
👉 데이터 무결성
데이터베이스 안의 데이터는 어떤 경로를 통해 들어왔든 오류가 있어서는 안 되는데 이를 무결성(Integrity)이라고 합니다.
무결성을 지키기 위해 데이터베이스는 제약 조건(constraint)을 따릅니다.
예를 들어 학생 데이터에서 모든 학생은 학번이 반드시 있어야 하고 학번이 중복되면 안 된다는 제약 조건을 생각해봅시다.
이 제약 조건을 충실히 지킨다면 학번으로도 학번으로도 학생 데이터에서 학생을 정확히 찾을 수 있습니다.
즉, 학번은 무결한 데이터를 보장하는 요소이며, 자동 발급기로 성적 증명서나 재학 증명서를 뗄 떼 학번만 조회해도 정확한 자료를 줄력할 수 있습니다.
👉 데이터의 독립성
데이터베이스의 크기를 변경하거나 데이터 파일의 저장소를 변경하더라도 기존에 작성된 응용 프로그램은 전혀 영향을 받지 않습니다.
즉 데이터베이스와 응용 프로그램은 서로 의존적인 관계가 아니라 독립적인 관계입니다.
예를 들어 데이터베이스가 저장된 디스크가 새것으로 변경되어도 기존에 사용하던 응용 프로그램은 아무런 변경 없이 계속 사용할 수 있습니다.
👉 보안
데이터베이스 안에 데이터는 아무나 접근할 수 있는 것이 아니라 데이터를 소유한 사람이나 데이터에 접근이 허가된 사람만 접근할 수 있습니다.
또한, 같은 데이터에 접근할 때도 사용자의 계정에 따라서 각각 다른 권한을 갖습니다.
최근 들어 고객 정보 유출 사고가 빈번하여 보안(Security)은 데이터베이스에서 더욱 중요한 이슈가 되고 있습니다.
👉 데이터 중복 최소화
데이터베이스에서는 동일한 데이터가 여러 군데 중복 저장되는 것을 방지합니다.
학교를 예로 들면, 학생 정보를 이용하는 교직원들(학생처, 교무처, 과사무실 등)이 각 직원마다 별도의 엑셀 파일로 학생 정보를 관리하면 한 명의 학생 정보가 각각의 엑셀 파일에 중복 저장됩니다.
그러나 데이터베이스에 통합하여 관리하면 하나의 테이블에 데이터를 저장한 후 응용 프로그램마다 이를 공유하여 사용할 수 있어 데이터의 중복을 최소화할 수 있습니다.
👉 응용 프로그램 제작 및 수정 용이
기존 파일 시스템에서는 각각의 파일 포맷에 맞춰 응용 프로그램을 개발했습니다.
그러나 데이터베이스를 이용하면 통일된 방식으로 응용 프로그램을 작성할 수 있고 유지,보수 또한 쉽습니다.
👉 데이터의 안전성 향상
대부분의 DBMS는 데이터의 백업/복원 기능을 제공합니다.
따라서 데이터가 손상되는 문제가 발생하더라도 원래의 상태로 복원 또는 복구할 수 있습니다.
-
-
-
-
-
📝[Post] 서버와 클라이언트의 개념(1)
🙋♂️ Preview
이번 포스트에서는 컴퓨터 과학에서 말하는 서버와 클라이언트의 개념을 크게 세 가지로 나눠 살펴보겠습니다.
이것은 이해를 돕기 위한 분류로, 서버와 클라이언트라는 개념에 익숙해지고 난 후에 다시 보면 왜 이렇게 나누었는지 이해가 될 것 입니다.
1️⃣ 네트워크에서의 서버와 클라이언트.
서버(Server) : “서비스를 제공하는 쪽”
클라이언트(Client) : “서비스를 제공받는 쪽”
그림에서 서버는 실제 존재하는 물리적인 고성능 컴퓨터이고, 클라이언트는 데스크톱이나 노트북, 스마트폰 등과 같은 사용자들의 단말기를 나타냅니다.
즉, 물리적 장치와 또 다른 물리적 장치 사이의 관계를 의미합니다.
이렇게 물리적인 장치 간에 서로 통신이 이루어지기 위해서는 “통신을 시작하는 쪽”이 “상대방의 네트워크 주소인 IP 주소를 알고 있어야 합니다.”
“클라이언트가 서버의 IP주소를 알고있어야 서버와 클라이언트로서의 관계를 맺을 수 있습니다.”
1️⃣ 트래픽(Traffic) 처리 방법.
우리가 컴퓨터나 스마트폰으로 이용하는 서비스들은 수백만 명 이상의 사용자가 동시에 사용하고 있는 경우가 대부분입니다.
그렇다면 이러한 서비스를 운영하는 서버가 모두 고성능일까요? 🤔
당연히 그렇지 않습니다 ❌
한꺼번에 수백만 명 이상의 사용자로부터 생기는 “트래픽(Traffic)”을 처리하기 위한 방법은 여러가지가 있습니다.
여기서는 가장 범용적이고 직관적인 방법 두 가지, “로드 밸런싱” 과 “캐시”에 대해 간단히 설명하겠습니다.
1️⃣ 로드 밸런싱(Load Balancing).
“로드 밸런싱(Load Balancing)” : 부하 분산.
즉, 서버에 가해지는 부하(Load)를 분산하는 것입니다.
사용자들의 트래픽을 여러 서버가 나눠 받도록 구성하며, 일반적으로 네트워크 장비인 “스위치(Switch)” 를 할당해 “로드 밸런싱”할 수 있습니다.
스위치에서 어떤 서버로 로드 밸런싱이 되도록 할지는 소프트웨어적으로 제어할 수 있습니다.
“로드 밸런싱” 은 “스위치” 라는 장비가 “클라이언트의 트래픽을 먼저 받아” 서 여러 대의 서버로 “분산” 해 주는 방식입니다.
이렇게 하면 부하가 분산되는 효과 외에도 스위치 뒤에 연결된 서버들을 필요에 따라 추가하거나 삭제할 수 있어 편리합니다.
2️⃣ 캐시(Cache).
“캐시(Cache)” : 비용이 큰 작업의 결과를 어딘가에 저장하여 비용이 작은 작업으로 동일한 효과를 내는 것.
캐시를 이용하면 매번 요청이 들어올 때마다 비용이 큰 작업을 다시 수행할 필요 없이 미리 저장된 결과로 응답하면 됩니다.
물론 이렇게 하면 가장 최신의 데이터는 아닐 수 있지만, 성능을 극대화시키고자 하는 캐시의 목적을 생각해 데이터의 실시간성을 조금 포기해도 되는 경우가 많습니다.
✏️ Example
음원 서비스
데이터베이스에 저장된 수많은 음원의 다운로드 수, 스트리밍 수, 추천 수 등으로 인기 점수를 계산하려 100갸의 곡을 오름차순 순위로 제공합니다.
만약 사용자가 한 번 음원을 조회할 때마다 모든 음원의 인기 점수를 계산해 순위를 매긴다면 아마 사용자가 수백 명만 되어도 서버 부하로 응답 시간이 매우 느려질 것입니다.
이렇게 수많은 음원의 인기 점수를 매번 계산하여 순위를 매기는 작업이 바로 ‘비용이 큰 작업’ 입니다.
매시 정각마다 TOP 100을 계산한 결과를 저장했다가 사용자의 요청이 들어왔을 때 응답해주면 ‘비용이 작은 작업’으로 대체할 수 있습니다.
사용자는 16시 30분에 16시에 저장된 TOP 100 결과로도 큰 불편함을 느끼지 않습니다.
이렇게 사용자가 캐시된 과거의 데이터를 보더라도 서비스 시용에 지장이 없다면 캐시 사용을 충분히 고려할 만합니다.
“캐시” 는 다양한 상황에서 비슷한 뜻으로 사용되지만, 공통적으로, ‘비용이 큰 작업을 비용이 작은 작업으로 대신하는 것’이라고 정리할 수 있습니다.
-
-
-
-
-
-
-
-
-
-
📦[DS,Algorithm] Circular Queue(원형 큐)의 중간 지점 찾기.
1️⃣ Circular Queue(원형 큐)의 중간 지점 찾기.
Java에서 배열을 사용하여 구현한 원형 큐에서 중간 지점을 찾는 방법은 큐의 시작 위치(‘front’)와 끝 위치(‘rear’)를 기준으로 계산할 수 있습니다.
중간 지점을 찾는 공식은 원형 큐의 특성을 고려하여 적절히 조정되어야 합니다.
2️⃣ 중간 지점을 찾기 위한 방법.
1️⃣ 중간 지점 계산 공식.
중간 지점을 찾는 방법은 큐의 시작점과 끝점을 이용하여 계산할 수 있습니다.
원형 큐의 크기, 시작 인덱스(front), 끝 인덱스(rear)를 사용하여 중간 인덱스를 계산할 수 있습니다.
이때 중간 지점을 계산하는 공식은 다음과 같습니다.
(front + size / 2) % capacity
여기서 ‘size’ 는 큐에 현재 저장된 요소의 수이고, ‘capacity’ 는 큐의 전체 크기입나다.
3️⃣ 예시
public class CircularQueue {
private int[] queue;
private int front, rear, size, capacity;
public CircularQueue(int capacity) {
this.capacity = capacity;
this.queue = new int[capacity];
this.front = 0;
this.rear = 0;
this.size = 0;
}
public boolean isFull() {
return size == capacity;
}
public boolean isEmpty() {
return size == 0;
}
public void enqueue(int data) {
if (isFull()) {
throw new RuntimeException("Queue is full");
}
queue[rear] = data;
rear = (rear + 1) % capacity;
size++;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int data = queue[front];
front = (front + 1) % capacity;
size--;
return data;
}
public int getMiddle() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int middleIndex = (front + size / 2) % capacity;
return queue[middleIndex];
}
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(5);
cq.enqueue(10);
cq.enqueue(20);
cq.enqueue(30);
cq.enqueue(40);
cq.enqueue(50);
System.out.println("Middle element: " + cq.getMiddle()); // Output: Middle element: 30
cq.dequeue();
cq.enqueue(60);
System.out.println("Middle element: " + cq.getMiddle()); // Output: Middle element: 40
}
}
이 코드에서는 ‘CircularQueue’ 클래스를 정의하고, ‘enqueue’, ‘dequeue’, ‘isFull’, ‘isEmpty’ 메서드를 포함합니다.
또한, 큐의 중간 요소를 반환하는 ‘getMiddle’ 메서드를 정의합니다.
이 메서드는 현재 큐의 크기와 시작 인덱스를 사용하여 중간 인덱스를 계산한 후 해당 인덱스의 요소를 반환합니다.
-
-
-
📦[DS,Algorithm] Deque에서의 front와 rear의 변화.
🧨 시발점.
Deque을 공부하던 중 동적으로 변하는 front와 rear가 근본적으로 어떻게 동작하는지 궁금해졌습니다.
이것을 알게되면 정확하게 Deque의 addFirst, addLast, removeFirst, removeLast 시 front와 rear가 어디에 위치하는지 알 수 있고 Deque의 원리를 이해 할 수 있을 것 같았습니다.
1️⃣ Deque의 front와 rear의 위치는 변할 수 있나요? 🤔
‘Deque‘ (Double Ended Queue)에서 ‘front‘ 와 ‘rear‘ 의 위치는 변할 수 있습니다.
‘Deque‘ 는 양쪽 끝에서 삽입과 삭제가 모두 가능한 자료구조이기 때문에, ‘front‘ 와 ‘rear‘ 의 위치는 데이터가 삽입되거나 제거될 때마다 변합니다.
2️⃣ Deque에서의 front와 rear의 변화. 🤩
1️⃣ 삽입 연산 (‘addFirst‘ 와 ‘addLast‘)
‘addFirst’ : 요소를 덱의 앞쪽에 삽입합니다.
‘front‘ 위치가 바뀝니다.
‘addLast’ : 요소를 덱의 뒤쪽에 삽입합니다.
‘rear‘ 위치가 바뀝니다.
2️⃣ 삭제 연산 (‘removeFirst‘ 와 ‘removeLast‘)
‘removeFirst’ : 덱의 앞쪽에서 요소를 제거합니다.
‘front‘ 위치가 바뀝니다.
‘removeLast’ : 덱의 뒤쪽에서 요소를 제거합니다.
‘rear‘ 위치가 바뀝니다.
3️⃣ 예제 코드.
아래는 ‘Deque’ 의 ‘LinkedList’ 구현을 사용하여 ‘front’ 와 ‘rear’ 의 변화를 보여주는 예제 코드입니다.
import java.util.Deque;
import java.util.LinkedList;
public class DequeExample {
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
// 요소를 덱의 앞과 뒤에 추가
deque.addFirst("A"); // front: A
deque.addLast("B"); // rear: B
deque.addFirst("C"); // front: C, rear: B
deque.addLast("D"); // rear: D
System.out.println("Initial Deque: " + deque); // 출력 : [C,A,B,D]
// 앞쪽에서 요소 제거
System.out.println("Removed from front: " + deque.removeFirst()); // 출력: C
// 뒤쪽에서 요소 제거
System.out.println("Removed from rear: " + deque.removeLast()); // 출력: D
System.out.println("Deque after removals: " + deque); // 출력: [A, B]
// 덱의 앞쪽과 뒤쪽에서 요소 확인
System.out.println("Front element: " + deque.getFirst()); // 출력: A
System.out.println("Rear element: " + deque.getLast()); // 출력: B
}
}
👉 설명.
1️⃣ 삽입 연산.
‘deque.addFirst(“A”)’ : “A”를 덱의 앞에 삽입합니다.
‘deque.addLast(“B”)’ : “B”를 덱의 뒤에 삽입합니다.
‘deque.addFirst(“C”)’ : “C”를 덱의 앞에 삽입합니다.
‘deque.addLast(“D”)’ : “D”를 덱의 뒤에 삽입합니다.
이 연산들은 ‘front’ 와 ‘rear’ 의 위치를 업데이트합니다.
2️⃣ 삭제 연산.
‘deque.removeFirst()’ : 덱의 앞쪽에서 “C”를 제거합니다.
‘deque.removeLast()’ : 덱의 뒤쪽에서 “D”를 제거합니다.
이 연산들은 ‘front’ 와 ‘rear’ 의 위치를 다시 업데이트합니다.
3️⃣ 요소 확인.
‘deque.getFirst()’ : 덱의 앞쪽 요소를 확인합니다.
‘deque.getLast()’ : 덱의 뒤쪽 요소를 확인합니다.
이 예시 코드는 ‘front’ 와 ‘rear’ 가 데이터의 삽입 및 삭제 연산에 따라 어떻게 변하는지 잘 보여줍니다.
‘Deque’ 는 이처럼 양쪽 끝에서의 삽입과 삭제 연산을 지원하므로, ‘front’ 와 ‘rear’ 의 위치는 동적입니다.
-
☕️[Java] 다형성(Polymorphism)
1️⃣ 다형성(Polymorphism).
‘다형성(Polymorphism)’ 은 ‘객체 지향 프로그래밍(OOP)’ 의 중요한 개념 중 하나로, 같은 인터페이스를 통해 서로 다른 데이터 타입의 객체를 조작할 수 있도록 합니다.
다형성은 코드의 재사용성과 유연성을 높여주며, 유지보수를 쉽게 해줍니다.
Java에서 ‘다형성’ 은 주로 ‘상속’ 과 ‘인터페이스’ 를 통해 구현됩니다.
2️⃣ 다형성의 개념.
다형성은 “하나의 인터페이스로 여러 가지 형태를 구현할 수 있는 능력” 을 의미합니다.
이는 같은 메서드가 다양한 객체에서 다르게 동작할 수 있게 합니다.
3️⃣ 다형성의 두 가지 형태.
1️⃣ 컴파일 시간 다형성(Compile-time Polymorphism)
메서드 오버로딩(Method Overloading)을 통해 구현됩니다.
컴파일 시점에 어떤 메서드가 호출될지 결정됩니다.
같은 이름의 메서드를 여러 개 정의하지만, 매개변수의 타입이나 개수가 달라야 합니다.
2️⃣ 런타임 다형성 (Runtime Polymorphism)
메서드 오버라이딩(Method Overriding)을 통해 구현됩니다.
실행 시점에 어떤 메서드가 호출될지 결정됩니다.
부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용합니다.
4️⃣ 컴파일 시간 다형성(Method Overloading).
메서드 오버로딩은 같은 클래스 내에서 같은 이름을 가진 메서드를 여러 개 정의하는 것입니다.
단, 매개변수의 수나 타입이 달라야 합니다.
💻 예제.
public class MathOperations {
// 정수 두 개의 합
public int add(int a, int b) {
return a + b;
}
// 실수 두 개의 합
public double add(double a, double b) {
return a + b;
}
// 새 개의 정수의 합
public int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
MathOperations mathOperations = new MathOperations();
System.out.println(mathOperations.add(1, 2)); // 3
System.out.println(mathOperations.add(1.5, 2.5)); // 4.0
System.out.println(mathOperations.add(1, 2, 3)); // 6
}
}
5️⃣ 런타임 다형성(Method Overriding).
메서드 오버라이딩은 자식 클래스가 부모 클래스의 메서드를 재정의하는 것을 말합니다.
이를 통해 자식 클래스의 객체가 부모 클래스의 메서드를 호출할 때, 자식 클래스의 메서드가 실행되도록 합니다.
💻 예제.
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // Animal 타입으로 Dog 객체 생성
Animal myCat = new Cat(); // Animal 타입으로 Cat 객체 생성
myDog.makeSound(); // Dog barks
myCat.makeSound(); // Cat meows
}
}
6️⃣ 인터페이스를 통한 다형성.
인터페이스를 통해서도 다형성을 구현할 수 있습니다.
인터페이스는 메서드의 서명만을 정의하며, 이를 구현하는 클래스가 메서드의 구체적인 동작을 정의합니다.
💻 예제.
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public class Main {
public static void main(String[] args) {
Shape myShape1 = new Circle();
Shape myShape2 = new Square();
myShape1.draw(); // Drawing a Circle
myShape2.draw(); // Drawing a Square
}
}
7️⃣ 다형성의 장점.
코드 재사용성 : 상위 클래스나 인터페이스를 사용하여 다양한 하위 클래스나 구현체를 다룰 수 있어 코드의 재사용성이 높아집니다.
유연성 : 새로운 클래스나 기능을 추가할 때 기존 코드를 수정할 필요 없이 확장할 수 있습니다.
유지보수성 : 코드를 이해하고 유지보수하는 것이 더 쉬워집니다. 메서드의 호출이 어디서 어떻게 이루어지는지 명확하기 때문입니다.
8️⃣ 예제: 다형성의 실질적 사용.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class PolymorphismExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
arrayList.add("ArrayList Item");
linkedList.add("LinkedList Item");
printList(arrayList); // ArrayList Item
printList(linkedList); // LinkedList Item
}
public static void printList(List<String> list) {
for (String item : list) {
System.out.println(item);
}
}
}
이 예제에서는 ‘List‘ 인터페이스를 사용하여 ‘ArrayList‘ 와 ‘LinkedList‘ 를 동일한 방식으로 처리합니다.
이를 통해 다양한 구현체를 다룰 수 있는 유연한 코드를 작성할 수 있습니다.
📝 결론.
다형성은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 코드의 유연성과 재사용성을 크게 향상시킵니다.
이를 통해 다양한 형태의 객체를 동일한 방식으로 다룰 수 있으며, 새로운 기능을 쉽게 확장하고 유지보수할 수 있습니다.
다형성은 상속과 인터페이스를 통해 구현되며, 메서드 오버로딩과 오버라이딩을 통해 다양한 형태를 취할 수 있습니다.
-
-
📦[DS,Algorithm] Circular Queue(원형 큐)란?
1️⃣ Circular Queue(원형 큐)란?
원형 큐는 큐의 일종으로, 배열을 사용하여 구현되며, 큐의 마지막 위치가 처음 위치와 연결되어 원형 구조를 가지는 큐입니다.
원형 큐는 고정된 크기의 배열을 사용하여 구현되므로, 큐의 마지막 인덱스가 배열의 끝에 도달하면 다음 인덱스가 배열의 시작 부분으로 이동합니다.
이를 통해 메모리를 효율적으로 사용할 수 있으며, 큐의 처음과 끝을 관리하는 데 도움이 됩니다.
2️⃣ 원형 큐의 원리.
고정된 크기 : 원형 큐는 고정된 크기의 배열을 사용하여 구현됩니다. 따라서 배열의 크기를 초과하여 요소를 추가할 수 없습니다.
연결된 인덱스 : 큐의 마지막 인덱스가 배열의 끝에 도달하면, 다음 인덱스는 배열의 처음 부분으로 이동합니다.
두 개의 포인터 : 원형 큐는 두 개의 포인터를 사용하여 구현됩니다.
‘front’ : 큐의 첫 번째 요소를 가리킵니다.
‘rear’ : 큐의 마지막 요소를 가리킵니다.
비어 있는 상태와 가득 찬 상태 : 큐가 비어 있는 상태와 가득 찬 상태를 구별해야 합니다. 이를 위해 추가적인 변수를 사용하거나 포인터의 위치를 비교하여 상태를 확인합니다.
3️⃣ 원형 큐의 주요 연산.
초기화 : 큐의 크기를 설정하고, ‘front’ 와 ‘rear’ 포인터를 초기화합니다.
isEmpty() : 큐가 비어 있는지 확인합니다.
isFull() : 큐가 가득 찼는지 확인합니다.
enqueue() : 큐에 요소를 추가합니다. ‘rear’ 포인터를 업데이트합니다.
dequeue() : 큐에서 요소를 제거하고 반환합니다. ‘front’ 포인터를 업데이트합니다.
peek() : 큐의 첫 번째 요소를 반환합니다.
4️⃣ 원형 큐의 예제 구현.
public class CircularQueue {
private int[] queue;
private int front;
private int rear;
private int size;
private int capacity;
// 생성자
public CircularQueue(int capacity) {
this.capacity = capacity;
queue = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
// 큐가 비어 있는지 확인
public boolean isEmpty() {
return size == 0;
}
// 큐가 가득 찼는지 확인
public boolean isFull() {
return size == capacity;
}
// 큐에 요소 추가
public void enqueue(int element) {
if (isFull()) {
System.out.println("Queue is full");
return;
}
rear = (rear + 1) % capacity;
queue[rear] = element;
size++;
}
// 큐에서 요소 제거
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty");
return -1;
}
int element = queue[front];
front = (front + 1) % capacity;
size--;
return element;
}
// 큐의 첫 번째 요소 확인
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty");
return -1;
}
return queue[front];
}
// 큐의 크기 반환
public int getSize() {
return size;
}
// 큐의 모든 요소 출력
public void display() {
if (isEmpty()) {
System.out.println("Queue is empty");
return;
}
int i = front;
int count = 0;
while (count < size) {
System.out.print(queue[i] + " ");
i = (i + 1) % capacity;
count++;
}
System.out.println();
}
// 메인 메서드 (테스트용)
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(5);
cq.enqueue(10);
cq.enqueue(20);
cq.enqueue(30);
cq.enqueue(40);
cq.enqueue(50);
cq.display(); // 출력: 10 20 30 40 50
System.out.println("Dequeued: " + cq.dequeue()); // 출력: Dequeued: 10
System.out.println("Dequeued: " + cq.dequeue()); // 출력: Dequeued: 20
cq.display(); // 출력: 30 40 50
cq.enqueue(60);
cq.enqueue(70);
cq.display(); // 출력: 30 40 50 60 70
System.out.println("Front element: " + cq.peek()); // 출력: Front element: 30
}
}
🙋♂️ 설명.
큐 초기화:
‘capacity’ : 큐의 최대 크기입니다.
‘queue’ : 큐를 저장할 배열입니다.
‘front’ : 큐의 첫 번째 요소를 가리키는 인덱스입니다.
‘rear’ : 큐의 마지막 요소를 가리키는 인덱스입니다.
‘size’ : 큐에 있는 요소의 개수입니다.
메서드:
‘isEmpty()’ : 큐가 비어 있는지 확인합니다.
‘isFull()’ : 큐가 가득 찼는지 확인합니다.
‘enqueue(int element)’ : 큐에 요소를 추가합니다.
‘dequeue()’ : 큐에서 요소를 제거하고 반환합니다.
‘peek()’ : 큐의 첫 번째 요소를 반환합니다.
‘getSize()’ : 큐에 있는 요소의 개수를 반환합니다.
‘display()’ : 큐의 모든 요소를 출력합니다.
5️⃣ 결론.
원형 큐는 배열을 효율적으로 사용하여 큐의 크기를 고정하고, 처음과 끝이 연결된 형태로 큐를 관리하는 자료구조입니다.
이를 통해 큐의 공간을 최대한 활용하고, 큐가 비어 있는지 가득 찼는지를 쉽게 확인할 수 있습니다.
🤔 궁금했던 부분.
rear = (rear + 1) % capacity;
1️⃣ 이 코드에서 % capacity 를 하는 이유는 무엇일까?
원형 큐에서 ‘rear’ 포인터를 업데이트 할 때 % capacity 를 사용하는 이유는 큐가 마지막 인덱스에 도달한 후, 다시 처음 인덱스로 돌아가도록 하기 위해서입니다.
이를 통해 큐가 원형으로 동작할 수 있습니다.
구체적으로 말하면, 큐의 크기를 고정된 크기의 배열로 구현할 때, 배열의 끝에 도달했을 때 다시 처음으로 돌아가는 기능을 제공합니다.
2️⃣ % 연산자의 역할.
배열의 인덱스는 0부터 시작하여 배열의 크기보다 1 작은 값까지입니다.
예를 들어, 배열의 크기가 5라면 인덱스는 0부터 4까지입니다.
원형 큐에서 새로운 요소를 추가할 때마다 ‘rear’ 포인터를 증가시키는데, 이 포인터가 배열의 끝을 넘어가지 않도록 해야 합니다.
이를 위해 % capacity 연산을 사용합니다.
rear = (rear + 1) % capacity;
이 연산은 ‘rear’ 포인터를 1씩 증가시키다가, 배열의 끝에 도달하면 다시 0으로 돌아가게 합니다.
즉, 배열의 인덱스가 배열의 크기를 넘어가면, 다시 처음 인덱스(0)로 순환되게 합니다.
👉 예제.
배열의 크기가 5인 원형 큐를 생각해봅시다.
초기 상태: ‘rear = -1’
요소 추가 시, ‘rear’ 포인터의 변화를 관찰해보면
첫 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 0’
두 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 1’
세 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 2’
네 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 3’
다섯 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 4’
여섯 번째 추가: ‘rear = (rear + 1) % 5 -> rear = 0’ (다시 처음으로 돌아감)
이렇게 ‘rear’ 포인터가 배열의 끝에 도달하면 다시 배열의 시작 부분으로 순환되므로, 배열을 효율적으로 사용할 수 있게 됩니다.
💻 코드 예제.
위 개념을 이용한 원형 큐의 ‘enqueue’ 메서드 구현
public void enqueue(int element) {
if (isFull()) {
System.out.println("Queue is full");
return;
}
rear = (rear + 1) % capacity; // rear 포인터를 증가시키고, 배열의 처음으로 순환시킴.
queue[rear] = element;
size++;
}
6️⃣ 정리.
원형 큐에서 ’% capacity’ 연산은 ‘rear’ 포인터와 ‘front’ 포인터가 배열의 끝에 도달했을 때, 다시 배열의 시작 부분으로 돌아가기 위해 사용됩니다.
이를 통해 배열의 고정된 크기를 효율적으로 활용하며, 원형 큐의 특성을 유지할 수 있습니다.
-
-
☕️[Java] 제네릭(Generic)
1️⃣ 제네릭(Generic)
Java에서의 제네릭(Generic) 은 클래스나 메서드에서 사용할 데이터 타입을 나중에 지정할 수 있도록 하는 기능입니다.
제네릭을 사용하면 코드의 재사용성을 높이고, 컴파일 시 타입 안전성을 제공하며, 명시적 타입 캐스팅을 줄일 수 있습니다.
2️⃣ 제네릭(Generic)의 주요 개념.
타입 매개변수 :
제네릭 클래스나 메서드는 타입 매개변수를 사용하여 타입을 정의합니다. 이 타입 매개변수는 클래스나 메서드가 호출될 때 구체적인 타입으로 대체됩니다.
타입 안정성 :
제네릭을 사용하면 컴파일 시 타입을 검사하므로, 런타입에 발생할 수 있는 타입 오류를 줄일 수 있습니다.
재사용성 :
제네릭 클래스나 메서드는 다양한 타입에 대해 동작하도록 설계할 수 있어, 코드의 재사용성을 높입니다.
3️⃣ 제네릭 클래스.
제네릭 클래스는 클래스 선언에 타입 매개변수를 포함하여 정의합니다.
일반적으로 타입 매개변수는 한 글자로 표현 되며, ‘T(Tyep)‘, ‘E(Element)‘, ‘K(Key)‘, ‘V(Value)‘ 등이 자주 사용됩니다.
예제.
// Box 클래스
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// Main 클래스
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println("String item: " + stringBox.getItem()); // String item: Hello
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println("Integer item: " + integerBox.getItem()); // Integer item: 123
}
}
4️⃣ 제네릭 메서드.
제네릭 메서드는 메서드 선언 타입 매개변수를 포함하여 정의합니다.
예제.
public class GenericMethodExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C", "D"};
printArray(intArray); // 1 2 3 4 5
printArray(strArray); // A B C D
}
}
5️⃣ 제네릭 타입 제한 (Bounded Type Parameters)
제네릭 타입 매개변수에 제한을 걸어 특정 타입의 하위 클래스나 인터페이스만 허용할 수 있습니다.
상한 제한 (Upper Bound)
public class BoundedTypeExample<T extends Number> {
private T number;
public BoundedTypeExample(T number) {
this.number = number;
}
public void printNumber() {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
BoundedTypeExample<Integer> intExample = new BoundedTypeExample<>(123);
intExample.printNumber(); // Number: 123
BoundedTypeExample<Double> doubleExample = new BoundedTypeExample<>(45.67);
doubleExample.printNumber(); // Number: 45.67
}
}
여기서 ‘T’ 는 ‘Number’ 클래스나 그 하위 클래스만 될 수 있습니다.
하한 제한 (Lower Bound)
하한 제한은 와일드카드(’? super T‘)를 사용하여 정의됩니다.
예를 들어 ‘List<? super Integer>‘ 는 ‘Integer‘ 의 상위 타입인 ‘Number‘, ‘Object‘ 등이 될 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public class LowerBoundWildcardExample {
public static void addNumbers(List<? super Integer> list) {
for (int i = 0; i < 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [0, 1, 2, 3, 4]
}
}
6️⃣ 제네릭의 제한 사항.
Primitive Type 사용 불가 : 제네릭은 참조 타입만 허용하며, 기본 타입은 사용할 수 없습니다.
// 올바르지 않음
Box<int> intBox = new Box<>(); // 컴파일 오류
정적 컨텍스트에서의 타입 매개변수 사용 : 정적 메서드나 정적 변수에서는 타입 매개변수를 사용할 수 없습니다.
public class GenericClass<T> {
private static T item; // 컴파일 오류
}
제네릭 배열 생성 불가 : 제네릭 배열을 직접 생성할 수 없습니다.
// 올바르지 않음
T[] array = new T[10]; // 컴파일 오류
제네릭은 Java의 강력한 기능으로, 타입 안전성을 높이고 코드의 재사용성을 극대화할 수 있게 해줍니다.
이를 적절히 활용하면 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
-
-
-
📦[DS,Algorithm] LinkedList를 사용한 Deque.
1️⃣ LinkedList를 사용한 Deque.
‘LinkedList‘ 는 ‘Deque‘ 인터페이스를 구현한 클래스 중 하나로, 양쪽 끝에서 삽입과 삭제가 가능한 이중 연결 리스트 기반의 자료 구조입니다.
‘LinkedList‘ 는 ‘Deque‘ 뿐만 아니라 ‘List‘, ‘Queue‘ 인터페이스도 구현하여 다양한 형태로 사용할 수 있습니다.
2️⃣ 주요 특징.
이중 끝 큐 : 양쪽 끝에서 요소를 추가하고 제거할 수 있습니다.
이중 연결 리스트 : 각 노드는 이전 노드와 다음 노드를 가리키는 두 개의 포인터를 가집니다.
비동기적 : ‘LinkedList‘ 는 비동기적으로 동작하므로 동기화된 환경에서 안전하지 않습니다.
3️⃣ 주요 메서드.
삽입 연산.
‘addFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘addLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
‘offerFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘offerLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
삭제 연산.
‘removeFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘removeLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
‘pollFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘pollLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
조회 연산.
‘getFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘getLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
‘peekFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘peekLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
스택 연산.
‘push(E e)’ : 스택의 맨 위에 요소를 추가합니다.(FIFO, First In First Out)
‘pop()’ : 스택의 맨 위에 있는 요소를 제거하고 반환합니다.(LIFO, Last In First Out)
4️⃣ 시간 복잡도
삽입과 삭제 연산 : ‘addFirst‘, ‘addLast‘, ‘removeFirst‘, ‘removeLast‘, ‘offerFirst‘, ‘offerLast‘, ‘pollFirst‘, ‘pollLast‘ 등의 연산은 O(1)입니다.
이중 연결 리스트를 사용하기 때문에 양쪽 끝에서의 삽입과 삭제는 상수 시간 내에 수행됩니다.
조회 연산 : ‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘ 등의 연산은 O(1)입니다.
임의 접근 연산( **‘get(int index)‘, ‘set(int index, E element)’ 등) :** 인덱스를 사용한 접근 연산은 리스트의 중간에 있는 요소를 찾기 위해 리스트를 순회해야 하므로 O(n) 시간이 걸립니다.
5️⃣ 코드 예시.
아래 코드는 ‘LinkedList‘ 를 ‘Deque‘ 로 사용하는 예제입니다.
import java.util.Deque;
import java.util.LinkedList;
public class LinkedListDequeExample {
public static void main(String[] args) {
// LinkedList로 Deque 생성
Deque<Integer> deque = new LinkedList<>();
// 요소 삽입
deque.addFirst(1);
deque.addLast(2);
deque.offerFirst(0);
deque.offerLast(3);
// 요소 조회
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("Peek first element: " + deque.peekFirst());
System.out.println("Peek last element: " + deque.peekLast());
// 요소 식제
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Removed last element: " + deque.removeLast());
System.out.println("Poll first element: " + deque.pollFirst());
System.out.println("Poll last element: " + deque.pollLast());
// 덱의 크기와 비어 있는지 여부 확인
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
// 스택 연산.
deque.push(4);
System.out.println("Pushed element: " + deque.peekFirst());
System.out.println("Popped element: " + deque.pop());
}
}
/*
=== 출력 ===
First element: 0
Last element: 3
Peek first element: 0
Peek last element: 3
Removed first element: 0
Removed last element: 3
Poll first element: 1
Poll last element: 2
Deque size: 0
Is deque empty? true
Pushed element: 4
Popped element: 4
*/
🙋♂️ 설명.
베열 초기화 : ‘DEFAULT_CAPACITY‘ 크기의 배열을 초기화하고, ‘head‘, ‘tail‘, ‘size‘ 변수를 초기화 합니다.
삽입 연산( **‘addFirst‘, ‘addLast‘) :** 요소를 덱의 첫 번째 또는 마지막에 추가합니다.
삭제 연산( **‘removeFirst‘, ‘removeLast‘) :** 첫 번째 요소와 마지막 요소를 각각 제거합니다.
조회 연산( **‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘) :** 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : ‘size‘ 와 ‘isEmpty‘ 메서드는 덱의 크기와 비어 있는지 여부를 반환합니다.
스택 연산( **‘push‘, ‘pop‘) :** 스택의 맨 위에 요소를 추가하고, 스택의 맨 위에 있는 요소를 제거하고 반환합니다.
위 예시 코드에서는 ‘LinkedList‘ 를 ‘Deque‘ 로 사용하여 다양한 연산을 수행하는 방법을 보여줍니다.
‘LinkedList‘ 는 이중 연결 리스트를 사용하기 때문에 양쪽 끝에서의 삽입과 삭제가 빠르고 효율적입니다.
-
-
📦[DS,Algorithm] ArrayDeque
1️⃣ ArrayDeque.
Java에서 ‘ArrayDeque‘ 는 ‘java.util‘ 패키지에 속하는 클래스이며, 큐(Queue)와 덱(Deque)의 기능을 모두 지원하는 배열 기반의 자료 구조입니다.
‘ArrayDeque‘ 는 ‘Deque‘ 인터페이스를 구현하며, 그기가 가변적인 배열을 사용하여 요소를 저장합니다.
2️⃣ 주요 특징.
이중 끝 큐 : 양쪽 끝에서 요소를 추가하고 제거할 수 있습니다.
크기 조정 : 필요에 따라 내부 배열의 크기를 자동으로 조정합니다.
스택 및 큐로 사용 가능 : ‘ArrayDeque‘ 는 스택(LIFO, Last In First Out)과 큐(FIFO, First In First Out) 모두로 사용할 수 있습니다.
비동기적 : ‘ArrayDeque‘ 는 비동기적으로 동작하므로 동기화된 환경에서 안전하지 않습니다.
3️⃣ 주요 메서드.
삽입 연산.
‘addFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘addLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
‘offerFirst(E e)’ : 지정된 요소를 덱의 앞쪽에 추가합니다.
‘offerLast(E e)’ : 지정된 요소를 덱의 뒤쪽에 추가합니다.
삭제 연산.
‘removeFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘removeLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
‘pollFirst()’ : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
‘pollLast()’ : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
조회 연산.
‘getFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘getLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
‘peekFirst()’ : 덱의 앞쪽에 있는 요소를 반환합니다.
‘peekLast()’ : 덱의 뒤쪽에 있는 요소를 반환합니다.
스택 연산.
‘push(E e)’ : 스택의 맨 위에 요소를 추가합니다.(LIFO, Last In First Out)
‘pop(E e)’ : 스택의 맨 위에 있는 요소를 제거하고 반환합니다.(LIFO, Last In First Out)
4️⃣ 시간 복잡도.
삽입과 삭제 연산 : ‘addFirst‘, ‘addLast‘, ‘removeFirst‘, ‘removeLast‘, ‘offerFirst‘, ‘offerLast‘, ‘pollFirst‘, ‘pollLast‘, 등의 연산은 평균적으로 O(1)입니다.
조회 연산 : ‘getFirst‘, ‘getLast‘, ‘peekFirst‘, ‘peekLast‘ 등의 연산은 O(1)입니다.
크기 조정 : 베열의 크기가 가득 찼을 때 크기를 두 배로 늘리거나 줄이는 작업은 O(n) 시간이 걸리지만, 이는 드물게 발생하므로 평균적으로는 O(1)로 간주합니다. (amortized O(1)).
5️⃣ 예제 코드
아래의 코드는 ‘ArrayDeque‘ 를 사용한 예제 코드입니다.
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeExample {
public static void main(String[] args) {
// ArrayDeque로 Deque 생성
Deque<Integer> deque = new ArrayDeque<>();
// 요소 삽입
System.out.println("=== 요소 삽입 ===");
deque.addFirst(1);
deque.addLast(2);
deque.offerFirst(0);
deque.offerLast(3);
System.out.println(deque);
System.out.println();
// 요소 조회
System.out.println("=== 요소 조회 ===");
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("Peek first element: " + deque.peekFirst());
System.out.println("Peek last element: " + deque.peekLast());
System.out.println();
// 요소 삭제
System.out.println("=== 요소 삭제 ===");
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Removed last element: " + deque.removeLast());
System.out.println("Poll first element: " + deque.pollFirst());
System.out.println("Poll last element: " + deque.pollLast());
System.out.println();
// 덱의 크기와 비어 있는지 여부 확인
System.out.println("=== 덱의 크기와 비어 있는지 여부 확인 ===");
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println();
// 스택 연산
System.out.println("=== 스택 연산 ===");
deque.push(4);
System.out.println("Pushed element: " + deque.peekFirst());
System.out.println("Popped element: " + deque.pop());
}
}
/*
=== 출력 ===
=== 요소 삽입 ===
[0, 1, 2, 3]
=== 요소 조회 ===
First element: 0
Last element: 3
Peek first element: 0
Peek last element: 3
=== 요소 삭제 ===
Removed first element: 0
Removed last element: 3
Poll first element: 1
Poll last element: 2
=== 덱의 크기와 비어 있는지 여부 확인 ===
Deque size: 0
Is deque empty? true
=== 스택 연산 ===
Pushed element: 4
Popped element: 4
*/
-
-
📦[DS,Algorithm] Deque(데크, 덱)
1️⃣ Deque(덱, Double Ended Queue)
Deque(덱, Double Ended Queue)는 양쪽 끝에서 삽입과 삭제를 할 수 있는 자료 구조입니다.
Java에서는 java.util 패키지에서 제공하는 Deque 인터페이스와 이를 구현한 클래스인 ArrayDeque 와 LinkedList 를 통해 사용할 수 있습니다.
Deque 는 큐(Queue)와 스택(Stack)의 기능을 모두 포함하고 있습니다.
1️⃣ 데크 기본 구조
데크의 기본 구조는 양방향에서 삽입 삭제 가능한 구조
일부 기능을 제한하여 용도에 맞게 변형 가능
add나 remove 계열은 예외를 발생시킵니다.
때문에 예외 처리가 가능합니다.
offer이나 poll 계열은 null이나 false를 반환합니다.
때문에 return값 (반환값)을 받아서 처리할 수 있습니다.
2️⃣ Deque의 주요 메서드.
1️⃣ 삽입 연산.
addFirst(E e) : 지정된 요소를 덱의 앞쪽에 추가합니다.
addLast(E e) : 지정된 요소를 덱의 뒤쪽에 추가합니다.
offerFirst(E e) : 지정된 요소를 덱의 앞쪽에 추가합니다.
offerLast(E e) : 지정된 요소를 덱의 뒤쪽에 추가합니다.
2️⃣ 삭제 연산.
removeFirst() : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
removeLast() : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
pollFirst() : 덱의 앞쪽에서 요소를 제거하고 반환합니다.
pollLast() : 덱의 뒤쪽에서 요소를 제거하고 반환합니다.
3️⃣ 조회 연산.
getFirst() : 덱의 앞쪽에 있는 요소를 반환합니다.
getLast() : 덱의 뒤쪽에 있는 요소를 반환합니다.
peekFirst() : 덱의 앞쪽에 있는 요소를 반환합니다.
peekLast() : 덱의 뒤쪽에 있는 요소를 반환합니다.
4️⃣ 기타 연산.
size() : 덱에 있는 요소의 수를 반환합니다.
isEmpty() : 덱이 비어 있는지 여부를 확인합니다.
3️⃣ 시간 복잡도.
Deque 인터페이스의 시간 복잡도는 이를 구현한 클래스에 따라 달라집니다.
Java에서는 주로 ArrayDeque 와 LinkedList 를 사용하여 Deque 를 구현합니다.
1️⃣ ArrayDeque
삽입과 삭제 연산 (앞과 뒤 모두): 평균적으로 O(1)
조회 연산 (앞과 뒤 모두): O(1)
ArrayDeque 는 배열을 기반으로 구현되기 때문에, 배열이 꽉 차면 자동으로 크기를 늘리지만, 이 과정은 amortized O(1)로 간주됩니다.
2️⃣ LinkedList
삽입과 삭제 연산 (앞과 뒤 모두): O(1)
조회 연산 (앞과 뒤 모두): O(1)
LinkedList 는 이중 연결 리스트로 구현되어 있어 각 노드가 이전과 다음 노드에 대한 참조를 가지고 있습니다.
LinkedList는 각 노드가 이전 노드와 다음 노드의 참조를 가지고 있어 삽입과 삭제가 O(1)의 시간 복잡도를 가집니다.
하지만 탐색에는 O(n)의 시간이 소요됩니다.
ArrayDeque는 배열을 사용하여 내부적으로 구현되기 때문에 삽입과 삭제 시에도 평균적으로 O(1)의 시간 복잡도를 가지며,
특히 큐의 끝에서의 연산이 빠릅니다.
다만, 내부적으로 배열이 가득 차면 크기를 조정해야 하므로 최악의 경우 O(n)의 시간 복잡도가 발생할 수 있습니다.
Deque 는 다양한 상황에서 유연하게 사용될 수 있는 유용한 자료구조입니다.
특히 양쪽 끝에서의 빠른 삽입과 삭제가 필요한 경우 유용합니다.
3️⃣ 직접 Deque 인터페이스 구현.
간단한 배열을 사용하여 Deque 를 구현해보겠습니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class SimpleArrayDeque<E> {
private static final int DEFALT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
public SimpleArrayDeque() {
elements = (E[]) new Object[DEFALT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addFirst(E e) {
if (size == elements.length) {
resize();
}
head = (head - 1 + elements.length) % elements.length;
elements[head] = e;
size++;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];
elements[head] = null; // for garbege collection
head = (head + 1);
size--;
return element;
}
public E removeLast() {
if (size == 0) {
throw new NoSuchElementException();
}
tail = (tail - 1 + elements.length) % elements.length;
E element = elements[tail];
elements[tail] = null; // for garbage collection
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + i) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
SimpleArrayDeque<Integer> deque = new SimpleArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.addFirst(0);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList === ");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Removed first element: " + deque.removeFirst());
System.out.println("Remove last element: " + deque.removeLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
}
}
/*
=== 출력 ===
=== dequeList ===
[0, 1, 2, 3]
First element: 0
Last element: 3
=== dequeList ===
[0, 1, 2, 3]
Removed first element: 0
Remove last element: 3
=== dequeList ===
[1, 2]
Deque size: 2
Is deque empty? false
=== dequeList ===
[1, 2]
*/
4️⃣ 입력 제한 Deque(Input-Restricted Deque).
입력 제한 Deque(Input-Restricted Deque)은 덱의 한쪽 끝에서만 삽입이 가능하고, 양쪽 끝에서 삭제가 가능한 자료구조입니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class InputRestrictedDeque<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public InputRestrictedDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];
elements[head] = null; // for garbage collection
head = (head + 1) % elements.length;
size--;
return element;
}
public E removeLast() {
if (size == 0) {
throw new NoSuchElementException();
}
tail = (tail - 1 + elements.length) % elements.length;
E element = elements[tail];
elements[tail] = null; // for gatbage collection
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
@SuppressWarnings("unchecked")
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + i) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
InputRestrictedDeque<Integer> deque = new InputRestrictedDeque<>();
deque.addLast(1);
deque.addLast(2);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList ===");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList ===");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Remove first element: " + deque.removeFirst());
System.out.println("Remove last elment: " + deque.removeLast());
System.out.println("=== dequeList ===");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
}
}
/*
=== 출력 ===
=== dequeList ===
[1, 2, 3]
First element: 1
Last element: 3
=== dequeList ===
[1, 2, 3]
Remove first element: 1
Remove last elment: 3
=== dequeList ===
[2]
Deque size: 1
Is deque empty? false
*/
1️⃣ 코드 설명.
배열 초기화 : DEFAULT_CAPACITY 크기의 배열을 초기화하고, head, tail, size 변수를 초기화합니다.
삽입 연산(addLast) : 요소를 덱의 마지막 에 추가합니다. 배열이 가득 차면 크기를 두 배로 늘립니다.
삭제 연산(removeFirst, removeLaste) : 첫 번째 요소와 마지막 요소를 각각 제거합니다.
조회 연산(getFirst, getLast) : 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : size 와 isEmpty 메서드는 덱의 크기와 덱이 비어 있는지 여부를 반환합니다.
배열 크기 조정 (resize) : 배열이 가득 찰 때 호출되며, 배열의 크기를 두 배로 늘리고 요소를 새 배열로 복사합니다.
이 예제에서는 요소를 덱의 끝에만 삽입할 수 있는 입력 제한 덱을 구현했습니다.
필요에 따라 이 구현을 확장하거나 수정하여 요구사항에 맞게 사용할 수 있습니다.
5️⃣ 출력 제한 Deque(Output-Restricted Deque).
출력 제한 Deque(Output-Restricted Deque)은 양쪽 끝에서 삽입이 가능하지만, 한쪽 끝에서만 삭제가 가능한 자료 구조입니다.
이 구조는 양쪽 끝에서 요소를 추가할 수 있지만, 삭제는 한쪽 끝에서만 할 수 있습니다.
import java.util.ArrayList;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class OutputRestrictedDeque<E> {
private static final int DEFAULT_CAPACITY = 10;
private E[] elements;
private int head;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public OutputRestrictedDeque() {
elements = (E[]) new Object[DEFAULT_CAPACITY];
head = 0;
tail = 0;
size = 0;
}
public void addFirst(E e) {
if (size == elements.length) {
resize();
}
head = (head - 1 + elements.length) % elements.length;
elements[head] = e;
size++;
}
public void addLast(E e) {
if (size == elements.length) {
resize();
}
elements[tail] = e;
tail = (tail + 1) % elements.length;
size++;
}
public E removeFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
E element = elements[head];;
elements[head] = null; // for garbage collection
head = (head + 1) % elements.length;
size--;
return element;
}
public E getFirst() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[head];
}
public E getLast() {
if (size == 0) {
throw new NoSuchElementException();
}
return elements[(tail - 1 + elements.length) % elements.length];
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
@SuppressWarnings("unchecked")
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[(head + 1) % elements.length];
}
elements = newElements;
head = 0;
tail = size;
}
public ArrayList<E> toArrayList() {
return IntStream.range(0, size)
.mapToObj(i -> elements[(head + i) % elements.length])
.collect(Collectors.toCollection(ArrayList::new));
}
}
// Main
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
OutputRestrictedDeque<Integer> deque = new OutputRestrictedDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.addFirst(0);
deque.addLast(3);
ArrayList<Integer> dequeList = deque.toArrayList();
System.out.println("=== dequeList === ");
System.out.println(dequeList);
System.out.println("First element: " + deque.getFirst());
System.out.println("Last element: " + deque.getLast());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Remove first element: " + deque.removeFirst());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
System.out.println("Deque size: " + deque.size());
System.out.println("Is deque empty? " + deque.isEmpty());
System.out.println("=== dequeList === ");
dequeList = deque.toArrayList();
System.out.println(dequeList);
}
}
/*
=== 출력 ===
=== dequeList ===
[0, 1, 2, 3]
First element: 0
Last element: 3
=== dequeList ===
[0, 1, 2, 3]
Remove first element: 0
=== dequeList ===
[1, 2, 3]
Deque size: 3
Is deque empty? false
=== dequeList ===
[1, 2, 3]
*/
1️⃣ 코드 설명.
배열 초기화 : DEFAULT_CAPACITY 크기의 배열을 초기화하고, head, tail, size 변수를 초기화 합니다.
삽입 연산(addFirst, addLast) : 요소를 덱의 첫 번째 또는 마지막에 추가합니다. 배열이 가득 차면 크기를 두 배로 늘립니다.
삭제 연산(removeFirst) : 첫 번째 요소를 제거합니다. 출력 제한 덱에서는 첫 번째 요소만 제거할 수 있습니다.
조회 연산(getFirst, getLast) : 첫 번째 요소와 마지막 요소를 반환합니다.
기타 메서드 : size 와 isEmpty 메서드는 덱의 크기와 덱이 비어 있는지 여부를 반환합니다.
배열 크기 조정(resize) : 배열이 가득 찰 때 호출되며, 배열의 크기를 두 배로 늘리고 요소를 새 배열로 복사합니다.
이 예제에서는 요소를 덱의 양쪽 끝에서 삽입할 수 있고, 첫 번째 요소만 제거할 수 있는 출력 제한 덱을 구현했습니다.
필요에 따라 이 구현을 확장하거나 수정하여 요구사항에 맞게 사용할 수 있습니다.
-
☕️[Java] IntStream
1️⃣ Java Docs - IntStream.
Module : java.base
Package : java.util.stream
Interface IntStream
All SuperInterfaces : AutoCloseble, BaseStream<Integer, IntStream>
AutoCloseble
BaseStream
Integer
IntStream
public interface IntStream extends BaseStream<Integer, IntStream>
순차 및 병렬 집계 연산을 지원하는 기본 int 값 요소의 시퀀스입니다. 이것은 Stream의 int 기본형 특수화입니다.
IntStream 이 Stream 의 한 형태로, int 값의 시퀀스를 처리하며 순차 및 병렬 연산을 지원한다는 의미입니다.
다음 예제는 Stream과 IntStream을 사용하여 빨간색 위젯의 무게 합계를 계산하는 집계 연산을 보여줍니다.
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
streams(스트림), stream operations(스트림 연산), stream pipelines(스트림 파이프라인), and parallelism(및 병렬 처리)에 대한 추가적인 명세는 Stream 클래스 문서와 java.util.stream 패키지 문서를 참조하십시오.
Since : 1.8
Nested Class Summary
Nested Classes
Modifier and Type: static interface
Interface: IntStream.Builder
Description: IntStream용 변경 가능한 빌더입니다.
2️⃣ IntStream.
IntStream 은 Java의 스트림 API(Stream API)의 일부로, 기본형 int 에 특화된 스트림을 나타냅니다.
IntStream 은 Java 8에서 도입된 스트림 API의 일부로, 컬렉션(리스트, 배열 등)과 같은 데이터 소스를 함수형 프로그래밍 스타일로 처리할 수 있게 해줍니다.
IntStream 은 Stream<Integer> 와는 달리 오토박싱과 언박싱의 오버헤드가 없는 것이 특징입니다.
🙋♂️ IntStream의 주요 기능
1. 생성:
IntStream 을 생성하는 방법은 여러가지가 있습니다.
예를 들어, 배열, 범위, 임의의 수 등을 사용하여 생성할 수 있습니다.
2. 연산:
스트림 연산은 두 가지로 나뉩니다.
중간 연산과 최종 연산.
중간 연산은 또 다른 스트림을 반환하고, 지연(lazy) 평가됩니다.
최종 연산은 스트림을 소비하여 결과를 반환합니다.
🙋♂️ IntStream 생성 방법.
1. of() 메서드:
고정된 개수의 int 값을 스트림으로 생성합니다.
IntStream stream = IntStream.of(1, 2, 3, 4, 5);
2. range() 및 rangeClosed() 메서드:
범위를 지정하여 스트림을 생성합니다. range 는 시작 값 포함, 끝 값 미포함, rangeClosed 는 시작 값과 끝 값을 모두 포함합니다.
IntStream stream = IntStream.range(0, 5); // 0, 1, 2, 3, 4, 5
IntStream closedStream = IntStream.rangeClosed(0, 5); // 0, 1, 2, 3, 4, 5
3. generate() 메서드:
람다 표현식을 사용하여 무한 스트림을 생성합니다.
🚨 주의: 무한 스트림은 반드시 제한을 걸아야 합니다.
IntStream stream = IntStream.generate(() -> 1).limit(5); // 1, 1, 1, 1, 1
4. iterate() 메서드:
초기값과 반복 함수로 스트림을 생성합니다.
IntStream stream = IntStream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8
5. builder() 메서드:
IntStream.Builder 를 사용하여 스트림을 생성합니다.
IntStream.Builder builder = IntStream.builder()l
builder.add(1).add(2).add(3).add(4).add(5);
IntStream stream = builder.builder();
6. 배열에서 생성:
배열을 스트림으로 변환합니다.
int[] array = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(array);
🙋♂️ IntStream의 주요 메서드.
1. 중간 연산:
map() : 각 요소에 함수 적용.
filter() : 조건에 맞는 요소만 통과
distinct() : 중복 요소 제거
sorted() : 정렬
limit() : 스트림 크기 제한
skip() : 처음 n개 요소 건너뛰기
2. 최종 연산:
forEach() : 각 요소에 대해 액션 수행
toArray() : 배열로 변환
reduce() : 모든 요소를 누적하여 하나의 값으로
collect() : 컬렉션으로 변환
sum() : 합계 연산
average() : 평균 계산
min(), max() : 최소, 최대값 찾기
count() : 요소 개수 반환
💻 예제 코드
예제 1: 0에서 5까지 거꾸로 출력.
import java.util.stream.IntStream;
public class Reverse {
public static void main(String[] args) {
IntStream.rangeClosed(0, 5)
.map(i -> 5 - i)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
5
4
3
2
1
0
*/
예제 2: 배열의 합계 계산
import java.util.stream.IntStream;
public class ArraySum {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
int sum = IntStream.of(array).sum();
System.out.println("sum = " + sum); // sum = 15
}
}
예제 3: 짝수 필터링
import java.util.stream.IntStream;
public class FilterEvenNumber {
public static void main(String[] args) {
IntStream.rangeClosed(1, 10)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
}
}
/*
=== 출력 ===
2
4
6
8
10
*/
📝 요약
IntStream 은 Java의 스트림 API의 일부분으로, 기본형 int에 특화된 스트림입니다.
이를 통해 컬렉션이나 배열을 함수형 프로그래밍 스타일로 처리할 수 있습니다.
IntStream 은 다양한 생성 방법과 중간 및 최종 연산을 제공하여 효율적이고 직관적인 데이터 처리를 가능하게 합니다.
📚 참고 문헌.
Java Docs - IntStream
-
-
-
📝[blog post] Java Docs 보는 방법.
📝 Java Docs를 읽는 능력이 필요한 이유. :)
저는 Documentation이 그 어떤 유명 테크 블로거의 글 보다 중요하고 심도있게 읽어야 한다는 개인적인 의견이 있습니다.
그 이유는 Java를 개발한 개발자분들이 직접 만든 설명서나 다름 없기 때문입니다.
우리가 레고를 생각해 봅시다.
내가 좋아하는 레고를 사서 집에서 조립할 때 무엇을 보나요? 🤔
맞습니다!
레고 패키지 안에 들어있는 “설명서”를 기반으로 레고를 조립합니다.
레고를 디자인하고 만드신 분이 직접 “이렇게 순서대로 만들면 당신이 원하는 멋진 레고 완성품을 얻을 수 있습니다!” 라는 것을 직.간접적으로 보여주는 아주 자세한 설명이 들어있죠 📝
설명서는 직접 디자인하고 설계한 사람의 철학과 그들이 왜 그렇게 만들었는지 그리고 어떻게 쓰여야하는지 정확, 명료하게 명시되어 있습니다.
또한 다른 구성품과 맞춰볼 수 있는 것도 제안하거나 보여주기도 합니다.
그래서 Documentation을 보고 제대로 활용할 줄 아는 것이 개발자에게는 중요한 능력 중 하나가 아닐까 하는 생각을 합니다 🙋♂️
1️⃣ Java Documentation 보기.
1. 온라인 문서.
Java SE Documentation은 Oracle 공식 사이트에서 제공됩니다.
Java 버전에 따라 다른 문서가 제공되니, 사용하는 Java 버전에 맞는 문서를 선택해야 합니다.
2. IDE 내장 문서.
많은 통합 개발 환경(IDE)에는 JavaDoc을 쉽게 볼 수 있는 기능이 내장되어 있습니다. InteillJ IDEA, Eclipes, NetBeans 등에서 코드 작성 시 JavaDocs를 볼 수 있습니다.
예를 들어, IntelliJ IDEA에서 클래스나 메소드 이름 위에 커서를 올리면 해당 클래스나 메소드의 JavaDoc이 팝업으로 표시됩니다.
3. 로컬 문서.
Java JDK를 설치할 때, JavaDoc을 로컬에 다운로드할 수 있습니다. 이를 통해 인터넷 연결 없이도 문서를 참조할 수 있습니다.
JDK 설치 경로 아래의 docs 폴더에 HTML 형식의 문서가 저장되어 있습니다.
2️⃣ Java Documentation 활용 방법
Java Documentation을 효과적으로 활용하는 방법을 알아봅시다.🤩
1. 클래스 및 메소드 탐색.
API 문서에서 패키지, 클래스, 메소드, 필드 등의 세부 정보를 탐색할 수 있습니다.
예를 들어, java.util 패키지에 어떤 클래스가 포함되어 있는지, ArrayList 클래스에 어떤 메소드가 있는지 등을 확인할 수 있습니다.
2. 사용 예제 찾기.
각 클래스와 메소드에는 사용 예제가 포함되어 있을 수 있습니다. 이러한 예제는 해당 API를 올바르게 사용하는 방법을 이해하는 데 도움이 됩니다.
3. 메소드 시그니처 및 설명.
메소드의 매개변수, 반환값, 예외 등을 설명하는 시그니처와 설명을 통해 메소드의 사용법을 정확히 알 수 있습니다.
예를 들어, String 클래스의 substring 메소드의 시그니처와 설명을 보면, 매개변수로 전달해야 할 값과 반환되는 값에 대한 정보를 얻을 수 있습니다.
4. 상속 구조 및 인터페이스.
클래스가 구현하는 인터페이스와 상속받는 클래스에 대한 정보를 확인할 수 있습니다. 이를 통해 클래스의 기능을 확장하거나 인터페이스를 구현하는 방법을 이해할 수 있습니다.
3️⃣ 예제
다음은 Java Documentation을 활용하는 몇 가지 예제입니다.
예제 1: ArrayList 클래스의 메소드 사용법 확인 🙋♂️
온라인 문서에서 ArrayList 클래스를 찾습니다.
Java SE Documentation에서 java.util.ArrayList 를 검색합니다.
ArrayList 클래스의 API 문서를 열어 메소드 목록을 확인합니다.
add(E e) 메소드 사용법 확인하기.
add(E e) 메소드는 리스트의 끝에 요소를 추가하는 메소드입니다.
메소드 설명을 읽고, 예제를 확인하여 사용법을 이해합니다.
예제 2. String 클래스의 substring 메소드 사용법 확인 🙋♂️
IDE 내장 문서 활용하기.
IntelliJ IDEA나 Eclipse에서 String 클래스의 substring 메소드를 사용하려고 할 때, 메소드 이름 위에 커서를 올리면 JavaDoc이 표시됩니다.
JavaDoc을 통해 substring(int beingIndex, int endIndex) 메소드의 매개변수와 반환 값에 대한 설명을 읽습니다.
public class Main {
public static void main(String[] args) {
String text = "Hello, World!";
String subText = text.substring(7, 12); // "World"
System.out.println(subText);
}
}
위 예제에서 substring 메소드의 매개변수가 beginIndex 와 endIndex 임을 알 수 있으며, 이는 시작 인덱스부터 종료 인덱스 전까지의 문자열을 반환합니다.
예제 3. 예외 처리 방법 확인 🙋♂️
예외 클래스 문서 확인하기.
java.lang.NullPointerException 클래스의 문서를 확인하여 언제 이 예외가 발생하는지, 그리고 이를 어떻게 처리할 수 있는지에 대한 정보를 얻습니다.
예외 처리 예제
public class Main {
public static void main(String[] args) {
try {
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Caught a NullPointerException");
}
}
}
이 예제는 NullPointException 이 발생할 때 이를 처리하는 방법을 보여줍니다.
📝 요약.
Java Documentation은 Java API를 이해하고 사용하는 데 필수적인 자료입니다.
Java Documentation를 온라인, IDE, 또는 로컬에서 접근할 수 있습니다.
API 문서를 통해 클래스와 메소드의 세부 정보를 확인하고, 예제를 참고하여 올바르게 사용하는 방법을 배울 수 있습니다.
상속 구조와 인터페이스 구현 방법을 이해하여 코드의 재사용성과 확장성을 높일 수 있습니다.
-
📦[DS,Algorithm] Java의 배열.
1️⃣ Java의 배열.
1️⃣ 배열이란 무엇인가?
배열(Array)은 동일한 타입의 여러 요소를 하나의 변수로 관리할 수 있게 해주는 자료구조입니다.
배열은 연속된 메모리 공간에 할당되며, 각 요소는 인덱스를 통해 접근할 수 있습니다.
2️⃣ 배열의 선언과 초기화.
Java에서 배열은 다음과 같이 선언하고 초기화할 수 있습니다.
int[] array = new int[5]; // 크기가 5인 정수형 배열 선언.
int[] array = {10, 20, 30, 40, 50}; // 초기화와 동시에 배열 선언
3️⃣ 배열의 요소와 접근.
배열의 각 요소는 인덱스를 통해 접근할 수 있으며, 인덱스는 0부터 시작합니다.
int firstElement = array[0]; // element = 10, 첫 번째 요소에 접근
array[1] = 25; // [10, 25, 30, 40, 50], 두 번째 요소에 값 25를 저장
4️⃣ 배열의 시간 복잡도.
배열의 시간 복잡도는 연산의 종류에 따라 다릅니다.
아래는 일반적인 배열 연산과 그 시간 복잡도를 설명한 것입니다.
1. 접근(Access)
특정 인덱스의 요소에 접근하는 시간 복잡도는 O(1)입니다.
이유 : 배열은 연속된 메모리 공간에 저장되므로 인덱스를 통해 바로 접근할 수 있기 때문입니다.
// 접근(Access)
int element = array[2];
// element = 30, time complexity = O(1)
// [10, 25, 30, 40, 50]
2. 탐색(Search)
배열에서 특정 값을 찾는 시간 복잡도는 O(n)입니다.
이유: 최악의 경우 배열의 모든 요소를 검사해야 할 수도 있기 때문입니다.
boolean found = false;
int target = 30;
for (int i = 0; i < array.length; i++) {
if (array[i] == target) { // i = 2, array[i] = 30
found = true;
break;
}
}
// [10, 25, 30, 40, 50]
3. 삽입(Insertion)
배열의 끝에 요소를 추가하는 시간 복잡도는 O(1)입니다.
배열의 특정 위치에 요소를 삽입하는 시간 복잡도는 O(n)입니다.
이유: 특정 위치에 삽입하기 위해서는 해당 위치 이후의 모든 요소를 한 칸씩 뒤로 밀어야 하기 때문입니다.
// 삽입(Insertion)
// 배열 삽입시 index가 array.length가 아니고 array.length - 1인 이유는
// array.length는 배열의 크기, 즉 5를 나타내기 때문입니다.
// index는 0부터 시작하기 때문에 배열의 크기가 5인 배열의 끝 index는 4입니다.
// 때문에 array.length - 1을 해줍니다.
array[array.length - 1] = 60; // 배열 끝에 삽입 (O(1)), [10, 25, 30, 40, 60]
// 배열 중간에 삽입하는 메서드
public static void insertion(int[] array, int index, int insertValue) {
// 배열 중간에 삽입(O(n))
for (int i = array.length - 1; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = insertValue;
System.out.println(Arrays.toString(array));
}
4. 삭제(Deletion)
배열의 끝에서 요소를 제거하는 시간 복잡도는 O(1)입니다.
배열의 특정 위치의 요소를 제거하는 시간 복잡도는 O(n)입니다.
이유: 특정 위치의 요소를 제거한 후에는 해당 위치 이후의 모든 요소를 한 칸씩 앞으로 당겨야 하기 때문입니다.
// 삭제(Deletion)
array[array.length - 1] = 0; // 배열의 끝에서 삭제 ((O(1)), [10, 25, 30, 77, 0]
System.out.println(Arrays.toString(array));
// 배열 중간에서 삭제하는 메서드
int deletionValue = deletion(array, 2);
System.out.println(deletionValue); // 30
// 배열 중간에 삭제하는 메서드
public static int deletion(int[] array, int index) {
// 배열 중간에 삭제(O(n))
int[] returnValue = new int[array.length];
for (int i = index, j = 0; i < array.length - 1 ; i++) {
returnValue[j] = array[i];
j++;
array[i] = array[i + 1];
}
array[array.length - 1] = 0; // 마지막 요소 초기화.
int deletionValue = returnValue[0]; // 배열을 메모리에서 지우기
returnValue = null;
return deletionValue;
}
5️⃣ 배열의 장점과 단점.
장점.
빠른 접근 속도 : 인덱스를 통해 O(1) 시간에 요소를 접근할 수 있습니다.
메모리 효율 : 연속된 메모리 공간을 사용하므로 메모리 사용이 효율적입니다.
단점.
고정된 크기 : 배열의 크기는 선언 시에 고정되므로, 실행 중에 크기를 변경할 수 없습니다.
삽입 및 삭제의 비효율성 : 배열 중간에 요소를 삽입하거나 삭제할 때 O(n)의 시간이 소요됩니다.
연속된 메모리 할당 필요 : 큰 배열을 사용할 떄는 연속된 메모리 공간이 필요하여 메모리 할당에 제한이 있을 수 있습니다.
배열은 이러한 특성들로 인해 빠른 접근이 필요한 상황에서는 매우 유용하지만, 삽입 및 삭제가 빈번히 일어나는 경우에는 비효율적일 수 있습니다.
따라서 상황에 맞게 적절한 자료구조를 선택하는 것이 중요합니다.
-
-
📦[DS,Algorithm] 배열에서 특정 인덱스의 요소를 삭제하기.
1️⃣ 배열에서 특정 인덱스의 요소를 삭제하기.
Java에서 배열의 특정 인덱스의 요소를 삭제하는 방법은 배열의 구조 특성상 직접적으로 제공되지 않습니다.
때문에 일반적으로 요소를 삭제하기 위해 다음의 방법을 사용합니다.
2️⃣ 배열에서 요소를 삭제하는 방법 2가지.
1️⃣ 새로운 배열을 생성하여 요소를 복사하는 방법 :)
● 특정 인덱스의 요소를 건너뛰고 나머지 요소를 새로운 배열에 복사합니다.
방법 1 : 새로운 배열 생성하여 복사.
// 배열의 특정 인덱스의 요소를 삭제하는 방법 - 1
// 방법1. 새로운 배열을 생성하여 요소를 복사하는 방법
// - 특정 인덱스의 요소를 건너뛰고 나머지 요소를 새로운 배열에 복사합니다.
public class Main {
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
array = removeElement(array, 0);
for (int value : array) {
System.out.println(value + " ");
}
}
// 특정 배열을 지우는 메소드
public static int[] removeElement(int[] array, int index) {
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
// 새로운 배열은 특정 요소를 지우기 때문에 기존 배열의 크기에서 -1 한 크기로 생성합니다.
int[] newArray = new int[array.length - 1];
for (int i = 0, j = 0; i < array.length; i++) {
if (i != index) {
newArray[j++] = array[i];
}
}
return newArray;
}
}
/*
=== 출력 ===
20
30
40
50
*/
“방법1의 장.단점”
새 배열 생성 : 메모리 사용량이 증가하지만, 원래 배열을 유지하고 싶은 경우 유용합니다.
2️⃣ 기존 배열을 이용하여 요소를 덮어쓰는 방법 :)
● 특정 인덱스 이후의 요소들을 앞으로 한 칸씩 이동시켜 덮어씁니다.
방법 2 : 기존 배열을 이용하여 요소 덮어쓰기.
// 배열의 특정 인덱스의 요소를 삭제하는 방법 - 2
// 방법2. 기존 배열을 이용하여 요소를 덮어쓰는 방법.
// - 특정 인덱스 이후의 요소들을 앞으로 한 칸씩 이동시켜 덮어 씁니다.
public class Main {
public static void main(String[] args) {
int[] array = {10, 20, 30, 40, 50};
array = removeElementInPlace(array, 0);
for (int value : array) {
System.out.println(value + " ");
}
}
public static int[] removeElementInPlace(int[] array, int index) {
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
for (int i = index; i < array.length - 1; i++) {
array[i] = array[i + 1];
}
// 배열의 마지막 요소를 0 또는 다른 기본값으로 설정 (선택 사항)
array[array.length - 1] = 0;
return array;
}
}
/*
=== 출력 ===
20
30
40
50
0
*/
“방법2의 장.단점”
기존 배열 사용 : 메모리를 절약할 수 있지만, 배열의 마지막 요소는 기본값으로 설정해야 합니다.
-
-
-
-
💾 [CS] 다양한 입출력 방법
1️⃣ 다양한 입출력 방법.
가장 보편적인 입출력 방법인 프로그램 입출력과 인터럽트 기반 입출력, DMA 입출력에 대해 알아보겠습니다.
1️⃣ 다양한 입출력 방법.
입출력 작업을 수행시 CPU와 장치 컨트롤러가 정보를 주고받아야 합니다.
여기에는 크게 세 가지 방법이 있습니다.
프로그램 입출력.
인터럽트 기반 입출력.
DMA 입출력.
2️⃣ 프로그램 입출력
프로그램 입출력(programmed I/O) 은 기본적으로 프로그램 속 명령어로 입출력 장치를 제어하는 방법입니다.
CPU가 프로그램 속 명령어를 실행하는 과정에서 입출력 명령어를 만나면 CPU는 입출력장치에 연결된 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다.
메모리에 저장된 정보를 하드 디스크에 백업하는 상황을 생각해 봅시다.
CPU는 대략 아래 과정으로 입출력 작업을 합니다.
1. ‘메모리에 저장된 정보를 하드 디스크에 백업한다’는 말은 ‘하드 디스크에 새로운 정보를 쓴다’는 말과 같습니다.
우선 CPU는 하드 디스크 컨트롤러의 제어 레지스터에 쓰기 명령을 보냅니다.
2. 하드 디스크 컨트롤러는 하드 디스크 상태를 확인합니다.
하드 디스크가 준비된 상태라면 하드 디스크 컨트롤러는 상태 레지스터에 준비되었다고 표시합니다.
3. (1) CPU는 상태 레지스터를 주기적으로 읽어 보며 하드 디스크의 준비 여부를 확인합니다.
(2) 하드 디스크가 준비됐음을 CPU가 알게 되면 백업할 메모리의 정보를 데이터 레지스터에 씁니다. 아직 백업 작업(쓰기 작업)이 끝나지 않았다면 (1)번부터 반복하고, 쓰기가 끝났다면 작업을 종료합니다.
이렇듯 프로그램 입출력 방식에서의 입출력 작업은 CPU가 장치 컨트롤러의 레지스터 값을 읽고 씀으로써 이루어집니다.
3️⃣ 메모리 맵 입출력과 고립형 입출력.
CPU 내부에 있는 레지스터들과 달리 CPU는 여러 장치 컨트롤러 속 레지스터들을 모두 알고 있기란 어렵습니다.
그렇다면 아래와 같은 명령어들은 어떻게 명령어로 표현되고, 메모리에 어떻게 저장 되어 있을까요?
프린터 컨트롤러의 상태 레지스터를 읽어라.
프린터 컨트롤러의 데이터 레지스터에 100을 써라.
키보드 컨트롤러의 상태 레지스터를 읽어라.
하드 디스크 컨트롤러의 데이어 레지스터에 ‘a’를 써라.
여기에는 크게 두 가지 방식이 있습니다.
바로 메모리 맵 입출력 과 고립형 입출력 입니다.
1️⃣ 메모리 맵 입출력.
메모리 맵 입출력(memory-mapped I/O) 은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 하나의 주소 공간으로 간주하는 방법입니다.
가령 1,024 개의 주소를 표현할 수 있는 컴퓨터가 있을 때 1,024개 전부 메모리 주소를 표현하는 데 사용하지 않습니다.
512개는 메모리 주소를, 512개는 장치 컨트롤러의 레지스터를 표현하기 위해 사용합니다.
주소 공간 일부를 아래와 같이 약속했다고 가정해 봅시다.
516번지: 프린터 컨트롤러의 데이터 레지스터
517번지: 프린터 컨트롤러의 상태 레지스터
518번지: 하드 디스크 컨트롤러의 데이터 레지스터
519번지: 하드 디스크 컨트롤러의 상태 레지스터
그렇다면 CPU는 ‘517번지를 읽어 들여라’라는 명령어로 키보드 상태를 읽을 수 있습니다.
그리고 ‘518 번지에 a를 써라’ 라는 명령어로 하드 디스크 컨트롤러의 데이터 레지스터로 데이터를 보낼 수 있습니다.
이때 중요한 점은 메모리 맵 입출력 방식에서 CPU는 메모리의 주소들이나 장치 컨트롤러의 레지스터들이나 모두 똑같이 메모리 주소를 대하듯 하면 된다는 점입니다.
그래서 메모리에 접근하는 명령어와 입출력장치에 접근하는 명령어는 굳이 다를 필요가 없습니다.
CPU가 ‘517번지를 읽어라’라는 명령어를 실행했을 때 517번지가 메모리상의 주소를 가리킨다면 CPU는 메모리 517번지에 저장된 정보를 읽어 들일 것이고, 517번지가 프린터 컽츠롤러의 상태 레지스터를 가리킨다면 CPU는 프린터의 상태를 확인할 수 있기 때문입니다.
2️⃣ 고립형 입출력.
고립형 입출력(isolated I/O) 은 메모리를 위한 주소 공간과 입출력장치를 위한 주소 공간을 분리하는 방법입니다.
가령 1,024개의 주소 공간을 가진 컴퓨터가 있다고 가정해 봅시다.
아래 그림처럼 제어 버스에 ‘메모리 읽기/쓰기’ 선 이외에 ‘입출력장치 읽기/쓰기’ 선이 따로 있다면 메모리에도 1,024 개의 주소 공간을 활용하고, 입출력장치도 1,024개의 주소 공간을 활용할 수 있습니다.
CPU가 메모리 읽기/쓰기 선이 활성화되는 명령어를 실행할 때는 메모리에 접근하고, 입출력장치 읽기/쓰기 선이 활성화되는 명령어를 실행할 때는 장치 컨트롤러에 접근하기 때문입니다.
고립형 입출력 방식에서 CPU는 입출력장치에 접근하기 위해 메모리에 접근하는 명령어와는 다른(입출력 읽기/쓰기 선을 활성화시키는) 입출력 명령어를 사용합니다.
메모리에 접근하는 명령어와 입출력장치에 접근하는 명령어는 굳이 다를 필요가 없었던 메모리 맵 입출력과 대조적입니다.
메모리 맵 입출력
고립형 입출력
메모리와 입출력장치는 같은 주소 공간 사용
메모리와 입출력장치는 분리된 주소 공간 사용
메모리 주소 공간이 축소됨
메모리 주소 공간이 축소되지 않음
메모리와 입출력장치에 같은 명령어 사용 가능
입출력 전용 명령어 사용
2️⃣ 인터럽트 기반 입출력
인터럽트는 ‘CPU가 입출력장치에 처리할 내용을 명령하면 입출력장치가 명령어를 수행하는 동안 CPU는 다른 일을 할 수 있다’라고 했습니다.
또한 ‘입출력장치가 CPU에게 인터럽트 요청 신호를 보내면 CPU는 하던 일을 잠시 멈추고 해당 인터럽트를 처리하는 프로그램인 인터럽트 서비스 루틴을 실행한 뒤 다시 하던 일로 되돌아온다’라고 했습니다.
입출력장치에 의한 하드웨어 인터럽트는 정확히 말하자면 입출력장치가 아닌 장치 컨트롤러에 의해 발생합니다.
CPU는 장치 컨트롤러에 입출력 작업을 명령하고, 장치 컨트롤러가 입출력장치를 제어하며 입출력을 수행하는 동안 CPU는 다른 일을 할 수 있습니다.
장치 컨트롤러가 입출력 작업을 끝낸 뒤 CPU에게 인터럽트 요청 신호를 보내면 CPU는 하던 일을 잠시 백업하고 인터럽트 서비스 루틴을 실행합니다.
이렇게 인터럽트를 기반으로 하는 입출력을 인터럽트 기반 입출력(Interrupt-Drive I/O) 이라고 합니다.
폴링
인터럽트와 자주 비교되는 개념 중 폴링(polling) 이라는 개념이 있습니다.
‘CPU는 주기적으로 장치 컨트롤러의 상태 레지스터를 확인하며 입출력장치의 상태를 확인한다’ 라고 했습니다.
이처럼 폴링이란 입출력장치의 상태는 어떤지, 처리할 데이터가 있는지를 주기적으로 확인하는 방식입니다.
폴링 방식은 당연하게도 인터럽트 방식보다 CPU의 부담이 더 큽니다.
인터럽트를 활용하면 CPU가 인터럽트 요청을 받을 때까지 온전히 다른 일에 집중할 수 있기 때문입니다.
이번에는 조금 더 일반적인 입출력장치가 많을 때를 생각해 봅시다.
예를 들어 키보드, 모니터, 스피커, 마우스를 사용하고 있다고 생각해봅시다.
이것은 컴퓨터 속 CPU가 동시다발적으로 발생하는 키보드, 마우스, 모니터, 스피커 인터럽트를 모두 처리해야 한다는 말이기도 합니다.
어떻게 여러 입출력장치에서 인터럽트가 동시에 발생한 경우에는 인터럽트들을 어떻게 처리해야 할까요?
간단하게 생각하면 인터럽트가 발생한 순서대로 인터럽트를 처리하는 방법이 있습니다.
가령 인터럽트 A를 처리하는 도중 발생한 또 다른 인터럽트 B의 요청을 받아들이지 않고, 인터럽트 A 서비스 루틴이 끝나면 그때 비로소 인터럽트 B 서비스 루틴을 실행하는 것이죠.
CPU가 플래그 레지스터 속 인터럽트 비트를 비활성화한 태 인터럽트를 처리하는 경우 다른 입출력장치에 의한 하드웨어 인터럽트를 받아들이지 않기 때문에 CPU는 이렇듯 순차적으로 하드웨어 인터럽트를 처리하게 됩니다.
하지만 현실적으로 모든 인터럽트를 전부 순차적으로만 해결할 수 없습니다.
인터럽트 중에서도 더 빨리 처리해야 하는 인터럽트가 있기 때문입니다.
즉, CPU는 인터럽트 간에 우선순위를 고려하여 우선순위가 높은 인터럽트 순으로 여러 인터럽트를 처리할 수 있습니다.
예를 들어 아래 그림과 같이 현재 CPU가 인터럽트 A를 처리하는 도중에 또 다른 인터럽트 B가 발생했다고 가정해 봅시다.
만약 지금 처리 중인 인터럽트 A보다 B의 우선순위가 낮다면 CPU는 A를 모두 처리한 뒤 B를 처리합니다.
하지만 인터럽트 A보다 B의 우선순위가 높다면 CPU는 인터럽트 A의 실행을 잠시 멈추고 인터럽터 B를 처리한 뒤 다시 A를 처리합니다.
플래그 레지스터 속 인터럽트 비트가 활성화되어 있는 경우, 혹은 인터럽트 비트를 비활성화해도 무시할 수 없는 인터럽트인 NMI(Non-Mashable Interrupt) 가 발생한 경우 CPU는 이렇게 우선순위가 높은 인터럽트부터 처리합니다.
우선순위를 반영하여 다중 인터럽트를 처리하는 방법에는 여러 가지가 있지만, 많은 컴퓨터에서는 프로그래머블 인터럽트 컨트롤러(PIC: Programmable Interrupt Controller) 라는 하드웨어를 사용합니다.
PIC 는 여러 장치 컨트롤러에 연결되어 장치 컨트롤러에서 보낸 하드웨어 인터럽트 요청들의 우선 순위를 판별한 뒤 CPU에 지금 처리해야 할 하드웨어 인터럽트는 무엇인지를 알려주는 장치입니다.
PIC에는 여러 핀이 있는데, 각 핀에는 CPU에 하드웨어 인터럽트 요청을 보낼 수 있는 약속된 하드웨어가 연결되어 있습니다.
가령 첫 번째 핀은 타이머 인터럽트를 받아들이는 핀, 두 번째 핀은 키보드 인터럽트를 받아들이는 핀… 이런 식으로 말이죠.
PIC에 연결된 장치 컨트롤러들이 동시에 하드웨어 인터럽트 요청을 보내면 PIC는 이들의 우선순위를 판단하여 CPU에 가장 먼저 처리할 인터럽트를 알려줍니다.
PIC의 다중 인터럽트 처리 과정을 조금 더 정확히 알아봅시다.
1. PIC가 장치 컨트롤러에서 인터럽트 요청신호(들) 를 받아들입니다.
2. PIC는 인터럽트 우선순위를 판단한 뒤 CPU에 처리해야 할 인터럽트 요청 신호를 보냅니다.
3. CPU는 PIC에 인터럽트 확인 신호를 보냅니다.
4. PIC는 데이터 버스를 통해 CPU에 인터럽트 벡터를 보냅니다.
5. CPU는 인터럽트 벡터를 통해 인터럽트 요청의 주체를 알게 되고, 해당 장치의 인터럽트 서비스 루틴을 실행합니다.
일반적으로 더 많고 복잡한 장치들의 인터럽트를 관리하기 위해 아래와 같이 PIC를 두 개 이상 계층적으로 구성합니다.
이렇게 PIC를 여러 개 사용하면 훨씬 더 많은 하드웨어 인터럽트를 관리할 수 있습니다.
참고로 PIC가 무시할 수 없는 인터럽트인 NMI까지 우선순위를 판별하지 않습니다.
NMI는 우선 순위가 가장 높아 우선순위 판별이 불필요하기 때문입니다.
PIC가 우선순위를 조정해주는 인터럽트는 인터럽트 비트를 통해 막을 수 있는 하드웨어 인터럽트입니다.
3️⃣ DMA 입출력
앞에 설명한 프로그램 기반 입출력과 인터럽트 기반 입출력의 공통점이 있다면 입출력장치와 메모리 간의 데이터 이동은 CPU가 주도하고, 이동하는 데이터도 반드시 CPU를 거친다는 점입니다.
예를 들어 입출력장치 데이터를 메모리에 저장하는 경우 CPU는 (1) 장치 컨트롤러에서 입출력장치 데이터를 하나씩 읽어 레지스터에 적재하고, (2) 적재한 데이터를 메모리에 저장합니다.
메모리 속 데이터를 입출력장치에 내보내는 경우도 마찬가지입니다.
CPU는 (1) 메모리에서 데이터를 하나씩 읽어 레지스터에 적재하고, (2) 적재한 데이터를 하나씩 입출력장치에 내보냅니다.
입출력장치와 메모리 사이에 전송되는 모든 데이터가 반드시 CPU를 거쳐야 한다면 가뜩이나 바쁜 CPU는 입출력장치를 위한 연산 때문에 시간을 뺏기게 됩니다.
하드 디스크 백업과 같이 대용량 데이터를 옮길 때는 CPU 부담이 더욱 커집니다.
그래서 입출력장치와 메모리가 CPU를 거치지 않고도 상호작용할 수 있는 입출력 방식인 DMA(Direct Memory Access) 가 등장하였습니다.
DMA는 이름 그대로 직접 메모리에 접근할 수 있는 입출력 기능입니다.
DMA 입출력을 하기 위해서는 시스템 버스에 연결된 DMA 컨트롤러 라는 하드웨어가 필요합니다.
DMA 입출력 과정.
일반적으로 DMA 입출력은 아래와 같이 이루어집니다.
(1) CPU는 DMA 컨트롤러에 입출력장치의 주소, 수행할 연산(읽기/쓰기), 읽거나 쓸 메모리 주소 등과 같은 정보로 입출력 작업을 명령합니다.
(2) DMA 컨트롤러는 CPU 대신 장치 컨트롤러와 상호작용하며 입출력 작업을 수행합니다. 이때 DMA 컨트롤러는 필요한 경우 메모리에 직접 접근하여 정보를 읽거나 씁니다.
(3) 입출력 작업이 끝나면 DMA 컨트롤러는 CPU에 인터럽트를 걸어 작업이 끝났음을 알립니다.
이번에는 메모리 내의 정보를 하드 디스크에 백업하는 작업이 DMA 입출력으로 어떻게 이루어지는지도 알아봅시다.
1. CPU는 DMA 컨트롤러에 하드 디스크 주소, 수행할 연산(쓰기), 백업할 내용이 저장된 메모리 주소 등의 정보와 함께 입출력 작업을 명령합니다.
2. (1) DMA 컨트롤러는 CPU를 거치지 않고 메모리와 직접 상호작용하며 백업할 정보를 읽어오고, (2) 이를 하드 디스크의 장치 컨트롤러에 내보냅니다.
3. 백업이 끝나면 DMA 컨트롤러는 CPU에게 인터럽트를 걸어 작업이 끝났음을 알립니다.
위 입출력 과정을 보면 알 수 있듯 입출력장치와 메모리 사이에 주고받을 데이터는 CPU를 거치지 않습니다.
CPU는 DMA 컨트롤러에게 입출력 작업 명령을 내리고, 인터럽트만 받으면 되기 때문에 작업 부담을 훨씬 줄일 수 있습니다.
다시 말해 CPU는 오직 입출력의 시작과 끝에만 관여하면 됩니다.
그런데 여기서 생각해 봐야 할 문제가 있습니다.
DMA 컨트롤러는 시스템 버스로 메모리에 직접 접근이 가능하지만, 시스템 버스는 동시 사용이 불가능합니다.
시트템 버스는 공용 자원이기 때문입니다.
CPU가 시스템 버스를 사용할 떄 DMA 컨트롤러는 시스템 버스를 사용할 수 없고, DMA 컨트롤러가 시스템 버스를 사용할 때는 CPU가 시스템 버스를 사용할 수 없습니다.
그래서 DMA 컨트롤러는 CPU가 시스템 버스를 이용하지 않을 때마다 조금씩 시스템 버스를 이용하거나, CPU가 일시적으로 시스템 버스를 이용하지 않도록 허락을 구하고 시스템 버스를 집중적으로 이용합니다.
CPU 입장에서는 마치 버스에 접근하는 주기를 도둑 맞는 기분이 들 겁니다. 그래서 이러한 DMA의 시스템 버스 이용을 사이클 스틸링(cycle stealing) 이라고 부릅니다.
입출력 버스
마지막으로 DMA 컨트롤러와 장치 컨트롤러의 연결 방식과 입출력 버스에 대해 알아봅시다.
CPU, 메모리, DMA 컨트롤러, 장치 컨트롤러가 모두 같은 버스를 공유하는 구성에서는 DMA를 위해 한 번 메모리에 접근할 때마다 시스템 버스를 두 번 사용하게 되는 부작용이 있습니다.
예로 들었던 메모리 내 정보를 하드 디스크로 백업하는 상황을 다시 생각해 봅시다.
이 경우 (1) 메모리에서 DMA 컨트롤러로 데이터를 가져오기 위해 시스템 버스를 한 번 사용하고, (2) DMA 컨트롤러의 데이터를 장치 컨트롤러로 옮기기 위해 시스템 버스를 또 한 번 사용합니다.
DMA를 위해 시스템 버스를 너무 자주 사용하면 그만큼 CPU가 시스템 버스를 이용하지 못합니다.
이 문제는 DMA 컨트롤러와 장치 컨트롤러들을 입출력 버스(input/output bus) 라는 별도의 버스에 연결하여 해결할 수 있습니다.
아래 그림과 같이 장치 컨트롤러들이 시스템 버스가 아닌 입출력 버스로 DMA 컨트롤러에 연결된다면 DMA 컨트롤러와 장치 컨트롤러가 서로 데이터를 전송할 때는 시스템 버스를 이용할 필요가 없으므로 시스템 버스의 사용 빈도를 줄일 수 있습니다.
현대 대부분 컴퓨터에는 입출력 버스가 있습니다.
다시 말해 대부분의 입출력장치(장치 컨트롤러)는 시스템 버스가 아닌 입출력 버스와 연결됩니다.
이런 점에서 볼 때 입출력 버스는 입출력장치를 컴퓨터 내부와 연결 짓는 통로라고도 볼 수 있습니다.
입출력 버스에는 PIC(Peripheral Component Interconnect) 버스, PCI Express(PCIe) 버스 등 여러 종류가 있습니다.
다음 그림은 여러 입출력 장치들을 PCIe 버스와 연결해 주는 통로인 PCIe 슬롯 입니다.
사용하는 거의 모든 입출력장치들은 이렇게 입출력 버스와 연결되는 통로를 통해 시스템 버스를 타고 CPU와 정보를 주고받습니다.
🙋♂️ 마무리.
키워드로 정리하는 핵심 포인트
프로그램 입출력은 프로그램 속 명령어로 입출력 작업을 하는 방식입니다.
메모리 맵 입출력은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 하나의 주소 공간으로 간주하는 입출력 방식입니다.
고립형 입출력은 메모리에 접근하기 위한 주소 공간과 입출력장치에 접근하기 위한 주소 공간을 별도로 분리하는 입출력 방식입니다.
인터럽트 기반 입출력은 인터럽트로써 입출력을 수행하는 방법입니다.
DMA 입출력은 CPU를 거치지 않고 메모리와 입출력장치 간의 데이터를 주고받는 입출력 방식입니다.
입출력 버스는 입출력장치와 컴퓨터 내부를 연결 짓는 톨로로, 입출력 작업 과정에서 시스템 버스 사용 횟수를 줄여줍니다.
-
-
📦[DS,Algorithm] 스택(Stack)
1️⃣ 스택(Stack).
스택(Stack)은 자료구조의 한 종류로, 데이터가 일렬로 쌓이는 구조를 가지고 있습니다.
1️⃣ 스택(Stack)의 특징.
“후입선출(LIFO, Last In First Out)”로, 가장 나중에 삽입된 데이터가 가장 먼저 꺼내진다는 점이 특징입니다.
2️⃣ 스택(Stack)의 기본 연산.
푸시(Push) : 스택의 맨 위에 데이터를 삽입하는 연산.
팝(Pop) : 스택의 맨 위에 있는 데이터를 제거하고 반환하는 연산.
3️⃣ 스택(Stack)의 부가적인 연산.
피크(peek) 또는 탑(top) : 스택의 맨 위에 있는 데이터를 제거하지 않고 반환하는 연산.
isEmpty : 스택이 비어 있는지 여부를 확인하는 연산.
size : 스택에 있는 데이터의 개수를 반환하는 연산.
4️⃣ 스택(Stack)의 실제 응용 사례.
웹 브라우저의 방문 기록(뒤로 가기 기능)
함수 호출시의 호출 스택
역폴란드 표기법 계산 등
5️⃣ 스택(Stack)의 구현.
스택은 배열이나 연결 리스트를 이용하여 구현할 수 있습니다.
배열을 이용한 스택 구현은 고정된 크기를 가지며, 연결 리스트를 이용한 스택 구현은 동적으로 크기를 조절할 수 있습니다.
배열을 이용한 스택 : 고정된 크기의 배열을 사용하여 스택을 구현할 수 있습니다. 이 경우 스택의 크기가 초과되면 더 큰 배열로 복사하는 추가 작업이 필요할 수 있습니다.
연결 리스트를 이용한 스택 : 동적으로 크기를 조절할 수 있는 연결 리스트를 사용하여 스택을 구현할 수 있습니다. 연결 리스트의 노드 삽입 및 삭제는 O(1)의 시간 복잡도를 가지므로, 스택 연산을 효율적으로 수행할 수 있습니다.
6️⃣ 시간 복잡도
스택의 각 연산은 일반적으로 다음과 같은 시간 복잡도를 가집니다.
Push : O(1)
데이터를 스택의 맨 위에 추가하는 연산은 항상 일정한 시간 내에 완료됩니다.
Pop : O(1)
데이터를 스택의 맨 위에서 제거하는 연산도 항상 일정한 시간 내에 완료됩니다.
Peek 또는 Top : O(1)
스택의 맨 위에 있는 데이터를 확인하는 연산은 데이터 접근만 필요하기 때문에 일정한 시간 내에 완료됩니다.
isEmpty : O(1)
스택이 비어 있는지 확인하는 연산은 스택의 크기만 확인하면 되프로 일정한 시간 내에 완료됩니다.
Size : O(1)
스택에 있는 데이터의 개수를 반환하는 연산은 스택의 크기 정보를 유지하고 있으면 일정한 시간 내에 완료됩니다.
7️⃣ 스택 구현.
// Stack
public class Stack {
private int maxSize; // 스택의 최대 크기
private int top; // 스택의 맨 위를 가리키는 인덱스
private int[] stackArray; // 스택을 저장할 배열
// 생성자
public Stack(int size) {
maxSize = size;
stackArray = new int[maxSize];
top = -1; // 스택이 비어있음을 나타냄
}
// 스택에 값을 푸시하는 메소드
public void push(int value) {
if (isFull()) {
System.out.println("스택이 가득 찼습니다.");
return;
}
stackArray[++top] = value;
}
// 스택에서 값을 팝하는 메소드
public int pop() {
if (isEmpty()) {
System.out.println("스택이 비어있습니다.");
return -1; // 에러를 나타내기 위해 -1 반환
}
return stackArray[top--];
}
// 스택의 맥 위 값을 반환하는 메소드
public int peek() {
if (isEmpty()) {
System.out.println("스택이 비어있습니다.");
return -1; // 에러를 나타내기 위해 -1 반환
}
return stackArray[top];
}
// 스택이 비어있는지 확인하는 메소드
public boolean isEmpty() {
return (top == -1);
}
// 스택이 가득 찼는지 확인하는 메소드
public boolean isFull() {
return (top == maxSize -1);
}
// 스택의 크기를 반환하는 메소드
public int size() {
return top + 1;
}
}
// Main
public class Main {
public static void main(String[] args) {
Stack stack = new Stack(5); // 크기가 5인 스택 생성
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
System.out.println("스택의 맨 위 값 : " + stack.peek());
System.out.println("스택의 크기 : " + stack.size());
while (!stack.isEmpty()) {
System.out.println("팝 : " + stack.pop());
}
System.out.println("스택의 크기 : " + stack.size());
}
}
/*
===출력===
스택의 맨 위 값 : 5
스택의 크기 : 5
팝 : 5
팝 : 4
팝 : 3
팝 : 2
팝 : 1
스택의 크기 : 0
*/
주요 메서드 설명
push(int value) : 스택의 맨 위에 값을 추가합니다. 스택이 가득 찼을 경우, 에러 메시지를 출력합니다.
pop() : 스택의 맨 위 값을 제거하고 반환합니다. 스택이 비어 있을 경우, 에러 메시지를 출력하고 -1을 반환합니다.
peek() : 스택의 맨 위 값을 반환하지만, 스택에서 제거하지는 않습니다. 스택이 비어 있을 경우, 에러 메시지를 출력하고 -1을 반환합니다.
isEmpty() : 스택이 비어 있는지 여부를 확인합니다.
isFull() : 스택이 가득 찼는지 여부를 확인합니다.
size() : 스택에 현재 저장된 데이터의 개수를 반환합니다.
-
-
💾 [CS] 장치 컨트롤러와 장치 드라이버
1️⃣ 장치 컨트롤러와 장치 드라이버.
1️⃣ 장치 컨트롤러.
입출력장치는 CPU, 메모리보다 다루기가 더 까다롭습니다.
여기에는 크게 두 가지 이유가 있습니다.
첫째, 입출력장치에는 종류가 너무나도 많습니다.
키보드, 모니터, USB 메모리, CD-ROM, SSD, 마우스, 스피커, 프린터 등 매우 많습니다.
장치가 이렇게 다양하면 자연스레 장치마다 속도, 데이터 전송 형식 등도 다양합니다.
따라서 다양한 입출력장치와 정보를 주고받는 방식을 규격화하기가 어렵습니다.
이는 마치 CPU와 메모리는 한국어를 사용하는데, 프린터는 영어, 스피커는 일본어, 모니터는 중국어를 사용하는 상황과 같습니다.
둘째, 일반적으로 CPU와 메모리의 데이터 전송률은 높지만 입출력장치의 데이터 전송률은 낮습니다.
여기서 전송률(transfer rate) 이란 데이터를 얼마나 빨리 교환할 수 있는지를 나타내는 지표입니다.
CPU와 메모리처럼 전송률이 높은 장치는 1초에도 수많은 데이터를 주고받을 수 있지만, 키보드나 마우스와 같은 상대적으로 전송률이 낮은 장치는 같은 시간 동안 데이터를 조금씩만 주고받을 수 있습니다.
전송률의 차이는 CPU와 메모리, 입출력 장치간의 통신을 어렵게 합니다.
장치 컨트롤러(Derive Controller)
물론 어떤 입출력장치는 CPU나 메모리보다 전송률이 높은 경우도 있습니다.
하지만 결과적으로 CPU나 메모리와 전송률이 비슷하지 않기 때문에 같은 어려움을 겪게 됩니다.
이와 같은 이유로 입출력장치는 컴퓨터에 직접 연결되지 않고 장치 컨트롤러(Drive Controller) 라는 하드웨어를 통해 연결됩니다.
장치 컨트롤러는 입출력 제어기(I/O Controller), 입출력 모듈(I/O Module) 등으로 다양하게 불립니다.
모든 입출력장치는 각자의 장치 컨트롤러를 통해 컴퓨터 내부와 정보를 주고받고, 장치 컨트롤러는 하나 이상의 입출력장치와 연결되어 있습니다.
예를 들어 하드 디스크 또한 장치 컨트롤러가 있습니다.
2️⃣ 장치 컨트롤러의 역할.
장치 컨트롤러는 대표적으로 다음과 같은 역할을 통해 앞에서 언급한 문제들을 해결합니다.
CPU와 입풀력장치 간의 통신 중개
오류 검출
데이터 버퍼링
입풀력장치 종류가 많이 정보 규격롸가 어려웠던 문제는 장치 컨트롤러가 일종의 번역가 역할을 함으로써 해결할 수 있습니다.
그 과정에서 장치 컨트롤러는 자신과 연결된 입출력장치에 문제는 없는지 오류를 검출하기도 합니다.
장치 컨트롤러의 세 번째 기능인 데이터 버퍼링은 무엇일까요?
버퍼링(buffering) 이란 전송률이 높은 장치와 낮은 장치 사이에 주고받는 데이터를 버퍼(buffer) 라는 임시 저장 공간에 저장하여 전송률을 비슷하게 맞추는 방법입니다.
쉽게 말해 버퍼링은 ‘버퍼에 데이터를 조금씩 모았다가 한꺼번에 내보내거나, 데이터를 한 번에 많이 받아 조금씩 내보내는 방법’이라고 보면 됩니다.
즉, 장치 컨트롤러는 일반적으로 전송률이 높은 CPU와 일반적으로 전송률이 낮은 입출력장치와의 전송률 차이를 데이터 버퍼일으로 완화합니다.
3️⃣ 장치 컨트롤러의 내부 구조.
이번에는 장치 컨트롤러의 간략화된 내부 구조를 살펴봅시다.
장치 컨트롤러 내부는 아래와 같습니다.
실제로는 이보다 복잡하지만, 기억해야 하는 것은 데이터 레지스터(data register) 와 상태 레지스터(status register), 제어 레지스터(control register) 세 가지 입니다.
데이터 레지스터는 CPU와 입출력장치 사이에 주고받을 데이터가 담기는 레지스터입니다.
앞서 장치 컨트롤러는 데이터 버퍼링으로 전송률 차이를 완화한다고 했습니다.
데이터 레지스터가 그 버퍼 역할을 합니다.
최근 주고받은 데이터가 많은 입출력장치에서는 레지스터 대신 RAM을 사용하기도 합니다.
상태 레지스터에는 입출력장치가 입출력 작업을 할 준비가 되었는지, 입출력 작업이 완료되었는지, 입출력장치에 오류는 없는지 등의 상태 정보가 저장됩니다.
제어 레지스터는 입출력장치가 수행할 내용에 대한 제어 정보와 명령을 저장합니다.
이 레지스터들에 담긴 값들은 버스를 타고 CPU나 다른 입출력장치로 전달되기도 하고, 장치 컨트롤러에 연결된 입출력장치로 전달됩니다.
2️⃣ 장치 드라이버
새로운 장치를 컴퓨터에 연결하려면 장치 드라이버를 설치해야 합니다.
1️⃣ 장치 드라이버
장치 드라이버(device driver) 란 장치 컨트롤러의 동작을 감지하고 제어함으로써 장치 컨트롤러가 컴퓨터 내부와 정보를 주고받을 수 있게 하는 프로그램입니다.
프로그램이기에 당연히 실행 과정에서 메모리에 저장됩니다.
장치 컨트롤러가 입출력장치를 연결하기 위한 하드웨어적인 통로라면, 장치 드라이버는 입출력장치를 연결하기 위한 소프트웨어적인 통로입니다.
컴퓨터가 연결된 장치의 드라이버를 인식하고 실행할 수 있다면 그 장치는 어떤 회사에서 만들어진 제품이든, 생김새가 어떻든 상관없이 컴퓨터 내부와 정보를 주고받을 수 있습니다.
반대로 장치 드라이버를 인식하거나 실행할 수 없는 상태라면 그 장치는 컴퓨터 내부와 정보를 주고받을 수 없습니다.
장치 드라이버를 인식하고 실행하는 주체
장치 드라이버를 인식하고 실행하는 주체는 정확히 말하자면 윈도우, macOS와 같은 운영체제입니다.
즉, 운영체제가 장치드라이버를 인식하고 실행할 수 있다면 그 장치는 컴퓨터 내부와 정보를 주고받을 수 있습니다.
장치 드라이버는 운영체제가 기본으로 제공하는 것도 있지만, 장치 제작자가 따로 제공하기도 합니다.
물론 장치 제작자가 장치 드라이버를 따로 제공하는 경우 입출력장치는 해당 드라이버를 직접 설치해야만 사용이 가능합니다.
3️⃣ 키워드로 정리하는 핵심 포인트
입출력장치는 장치 컨트롤러 를 통해 컴퓨터 내부와 정보를 주고받습니다.
장치 드라이버는 장치 컨트롤러가 컴퓨터 내부와 정보를 주고받을 수 있게 하는 프로그램입니다.
-
📦[DS,Algorithm] 트리(Tree)
1️⃣ 트리(Tree).
트리(Tree) 는 계층적 구조를 나타내는 자료구조로, 노드(Node)와 에지(Edge)로 구성됩니다.
트리는 사이클이 없는 연결 그래프(Connected Graph)이며, 계층적 데이터 표현에 매우 유용합니다.
트리는 부모-자식 관계를 가지며, 데이터의 조직화와 검색, 계층적 데이터 표현에 사용됩니다.
1️⃣ 트리의 구성 요소.
노드(Node) : 트리의 기본 단위로, 데이터를 저장합니다.
에지(Edge) : 노드와 노드를 연결하는 선으로, 부모-자식 관계를 나타냅니다.
루트(Root) : 트리의 최상위 노드로, 부모 노드가 없습니다.
부모(Parent) : 다른 노드를 가리키는 노드입니다.
자식(Child) : 부모 노드에 의해 가리켜지는 노드입니다.
잎(Leaf) : 자식 노드가 없는 노드입니다.
내부 노드(Internal Node) : 자식 노드가 있는 노드입니다.
레벨(Level) : 루트 노드에서 특정 노드까지의 에지 수를 나타냅니다.
높이(Height) : 트리의 최대 레벨을 의미합니다.
2️⃣ 트리의 특성.
계층적 구조 : 트리는 계층적 구조로 데이터를 조직화합니다.
사이클 없음 : 트리는 사이클이 없는 그래프입니다.
연결성 : 모든 노드는 하나의 연결된 구성 요소로 되어 있습니다.
한 개의 루트 노드 : 트리는 하나의 루트 노드를 가지며, 루트 노드는 트리의 시작점입니다.
3️⃣ 트리의 종류.
이진 트리(Binary Tree) : 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 트리입니다.
이진 탐색 트리(Binary Search Tree, BST) : 이진 트리의 일종으로, 왼쪽 자식 노드의 값이 부모 노드의 값보다 작고, 오른쪽 자식 노드의 값이 부모 노드의 값보다 큰 특성을 가집니다.
균형 이진 트리(Balanced Binary Tree) : AVL 트리, 레드-블랙 트리 등과 같이 트리의 높이가 균형을 이루도록 유지하는 트리입니다.
B-트리(B-Tree) : 데이터베이스와 파일 시스템에서 사용되는 트리로, 자식 노드의 수가 정해진 다진 트리(Multiway Tree)입니다.
힙(Heap) : 완전 이진 트리의 일종으로, 부모 노드의 값이 자식 노드의 값보다 크거나 작은 특성을 가집니다.
트라이(Trie) : 문자열 검색을 위한 트리 자료구조로, 접두사 검색에 유용합니다.
4️⃣ 트리의 주요 연산.
삽입(Insertion) : 트리에 새로운 노드를 추가합니다.
삭제(Deletion) : 트리에서 노드를 제거합니다.
탐색(Search) : 트리에서 특정 값을 찾습니다.
순회(Traversal) : 트리의 모든 노드를 방문합니다. 전위(Preorder), 중위(Inorder), 후위(Postorder), 레벨 순회(Level Order) 방식이 있습니다.
-
-
📦[DS,Algorithm] 완전 이진 트리(Complete Binary Tree)
1️⃣ 완전 이진 트리(Complete Binary Tree).
완전 이진 트리(Complete Binary Tree)는 이진 트리의 한 종류입니다.
1️⃣ 완전 이진 트리(Complete Binary Tree)의 특성.
모든 레벨이 완전히 채워져 있다.
마지막 레벨을 제외한 모든 레벨의 노드가 최대 개수로 채워져 있습니다.
마지막 레벨의 노드들은 왼쪽부터 오른쪽으로 채워져 있습니다.
노드의 배치
트리의 높이가 ‘h’ 라면, 마지막 레벨을 제외한 모든 레벨에는 ‘2^k’ 개의 노드가 있습니다. 여기서 ‘k’ 는 해당 레벨의 깊이 입니다.
마지막 레벨에는 1개 이상 ‘2^h’ 개 이하의 노드가 있으며, 이 노드들은 왼쪽부터 채워집니다.
2️⃣ 완전 이친 트리의 예.
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
위의 트리는 완전 이진 트리의 예입니다.
모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드들은 왼쪽부터 오른쪽으로 채워져있습니다.
3️⃣ 완전 이진 트리의 속성.
노드 수
높이가 ‘h’ 인 완전 이진 트리는 최대 ‘2^(h+1) - 1’ 개의 노드를 가질 수 있습니다.
마지막 레벨을 제외한 모든 노드는 ‘2^h - 1’ 개의 노드를 가집니다.
높이
노드 수가 ‘n’ 인 완전 이진 트리의 높이는 ‘O(log n)’ 입니다.
배열 표현
완전 이진 트리는 배열을 사용하여 쉽게 표현할 수 있습니다. 이는 힙 자료구조에서 많이 사용됩니다.
4️⃣ 배열을 통한 완전 이진 트리 표현
완전 이진 트리는 배열을 사용하여 효율적으로 표현할 수 있습니다.
노드의 인덱스를 기준으로 부모-자식 관계를 쉽게 파악할 수 있습니다.
노드의 인덱스 규칙
루트 노드 : 인덱스 0
인덱스 ‘i’의 오른쪽 자식 노드 : ‘2*i + 1’
인덱스 ‘i’의 부모 노드 : ‘(i - 1) / 2’
5️⃣ 예제 코드
아래는 완전 이진 트리를 배열로 표현하고, 이를 출력하는 간단한 예제 코드입니다.
public class CompleteBinaryTree {
public static void main(String[] args) {
int[] tree = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 트리 출력
printTree(tree);
}
// 배열로 표현된 완전 이진 트리 출력
public static void printTree(int[] tree) {
for (int i = 0; i < tree.length; i++) {
int leftChildIndex = 2 * i + 1;
int rightChildIndex = 2 * i + 2;
System.out.print("Node " + tree[i] + ": ");
if (leftChildIndex < tree.length) {
System.out.print("Left Child: " + tree[leftChildIndex] + ", ");
} else {
System.out.print("Left Child: null, ");
}
if (rightChildIndex < tree.length) {
System.out.print("Right Child: " + tree[rightChildIndex]);
} else {
System.out.print("Right Child: null");
}
System.out.println();
}
}
}
/* 출력
Node 1: Left Child: 2, Right Child: 3
Node 2: Left Child: 4, Right Child: 5
Node 3: Left Child: 6, Right Child: 7
Node 4: Left Child: 8, Right Child: 9
Node 5: Left Child: null, Right Child: null
Node 6: Left Child: null, Right Child: null
Node 7: Left Child: null, Right Child: null
Node 8: Left Child: null, Right Child: null
Node 9: Left Child: null, Right Child: null
*/
설명
트리 배열 초기화 : int[] tree = {1, 2, 3, 4, 5, 6, 7, 8, 9};
완전 이진 트리를 배열로 표현합니다.
트리 출력 : printTree(tree)
배열로 표현된 완전 이진 트리를 출력하는 함수입니다.
각 노드에 대해 왼쪽 자식과 오른쪽 자식을 출력합니다.
시간 복잡도
삽입(Insertion) : O(log n)
삭제(Deletion) : O(log n)
탐색(Search) : O(n) (일반적으로 완전 이진 트리는 탐색보다 삽입/삭제가 주된 연산입니다.)
완전 이진 트리는 데이터의 구조적 특성 때문에 힙과 같은 자료구조에서 많이 사용됩니다.
이는 효율적인 삽입 및 삭제 연산을 제공하며, 배열을 통한 표현이 간편하여 다양한 알고리즘에서 유용하게 사용됩니다.
-
📦[DS,Algorithm] 이진 트리(Binary Tree)
1️⃣ 이진 트리(Binary Tree).
이진 트리(Binary Tree) 는 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 트리 구조입니다.
이 두 자식 노드는 일반적으로 왼쪽 자식(Left Child) 과 오른쪽 자식(Right Child) 이라고 불립니다.
이진 트리는 다양한 응용 프로그램에서 중요한 자료구조입니다.
1️⃣ 이진 트리의 구성 요소.
노드(Node) : 데이터를 저장하는 기본 단위입니다.
루트(Root) : 트리의 최상위 노드입니다.
자식(Child) : 특정 노드로부터 연결된 하위 노드입니다.
부모(Parent) : 특정 노드를 가리키는 상위 노드입니다.
잎(Leaf) : 자식 노드가 없는 노드입니다.
서브트리(Subtree) : 특정 노드와 그 노드의 모든 자식 노드로 구성된 트리입니다.
2️⃣ 이진 트리의 종류.
포화 이진 트리(Full Binary Tree) : 모든 노드가 0개 또는 2개의 자식 노드를 가지는 트리입니다.
완전 이진 트리(Complete Binary Tree) : 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 노드는 왼쪽부터 채워져 있는 트리입니다.
높이 균형 이진 트리(Height-balanced binary Tree) : AVL 트리와 같이 각 노드의 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이가 1 이하인 트리입니다.
이진 탐색 트리(Binary Search Tree, BST) : 왼쪽 서브트리의 모든 노드가 루트 노드보다 작고, 오른쪽 서브 트리의 모든 노드가 루트 노드보다 큰 트리입니다.
3️⃣ 이진 트리의 주요 연산 및 시간 복잡도.
삽입(Insertion) : 새로운 노드를 트리에 추가합니다.
일반적인 경우 시간 복잡도 : O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도 : O(n)(편향된 트리에서)
삭제(Deletion) : 트리에서 특정 노드를 제거합니다.
일반적인 경우 시간 복잡도 : O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도: O(n)(편향된 트리에서)
탐색(Search) : 트리에서 특정 값을 찾습니다.
일반적인 경우 시간 복잡도: O(log n)(이진 탐색 트리에서)
최악의 경우 시간 복잡도 : O(n)(편향된 트리에서)
순회(Traversal) : 트리의 모든 노드를 방문합니다. 순회 방법에는 전위(Preorder), 중위(Inorder), 후위(Postorder) 순회가 있습니다.
시간 복잡도: O(n)(모든 노드를 방문하기 때문에)
4️⃣ 이진 트리의 예제
이진 탐색 트리(BST)의 구현
// TreeNode
public class TreeNode {
int data;
TreeNode left;
TreeNode right;
public TreeNode(int data) {
this.data = data;
this.left = null;
this.right = null;
}
}
// BinarySearchTree
public class BinarySearchTree {
private TreeNode root;
public BinarySearchTree() {
this.root = null;
}
// 삽입 연산
public void insert(int data) {
root = insertRec(root, data);
}
private TreeNode insertRec(TreeNode root, int data) {
if (root == null) {
root = new TreeNode(data);
return root;
}
if (data < root.data) {
root.left = insertRec(root.left, data);
} else if (data > root.data) {
root.right = insertRec(root.right, data);
}
return root;
}
// 탐색 연산
public boolean search(int data) {
return searchRec(root, data);
}
private boolean searchRec(TreeNode root, int data) {
if (root == null) {
return false;
}
if (root.data == data) {
return true;
}
if (data < root.data) {
return searchRec(root.left, data);
} else {
return searchRec(root.right, data);
}
}
// 중위 순회(Inorder Traversal)
public void inorder() {
inorderRec(root);
}
private void inorderRec(TreeNode root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.data + " ");
inorderRec(root.right);
}
}
}
// Main
public class Main {
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
bst.insert(50);
bst.insert(30);
bst.insert(20);
bst.insert(40);
bst.insert(70);
bst.insert(60);
bst.insert(80);
System.out.println("Inorder traversal of the BST:");
bst.inorder(); // 출력: 20 30 40 50 60 70 80
System.out.println("\nSearch for 40: " + bst.search(40)); // 출력: true
System.out.println("Search for 90: " + bst.search(90)); // 출력: false
}
}
-
-
📦[DS,Algorithm] 해시 테이블(Hash Table)
1️⃣ 해시 테이블(Hash Table).
해시 테이블(Hash Table)은 데이터를 키-값 쌍(key-value pairs)으로 저장하는 자료구조입니다.
해시 테이블은 해시 함수를 사용하여 키를 해시 값으로 변환하고, 이 해시 값을 인덱스로 사용하여 배열에서 값을 저장하거나 검색합니다.
이를 통해 데이터에 빠르게 접근할 수 있습니다.
1️⃣ 해시 테이블의 구성 요소.
키(key) : 각 데이터를 식별하기 위한 고유한 식별자입니다.
값(Value) : 키와 연관된 데이터입니다.
해시 함수(Hash Function) : 키를 입력으로 받아 해시 값을 출력하는 함수입니다. 이 해시 값은 보통 정수이며, 배열의 인덱스로 사용됩니다.
버킷(Bucket) : 해시 값에 의해 인덱싱되는 배열의 각 위치입니다. 각 버킷은 하나의 키-값 쌍 또는 충돌 처리를 위한 데이터 구조(예: 연결 리스트)를 저장할 수 있습니다.
2️⃣ 해시 함수의 역할.
해시 함수는 키를 고정된 크기의 해시 값으로 매핑합니다.
이상적인 해시 함수는 키를 균등하게 분포시키고, 충돌을 최소화합니다.
3️⃣ 충동(Collision)과 충돌 해결 방법.
두 개 이상의 키가 동일한 해시 값을 가질 때 충돌이 발생합니다.
해시 테이블은 이러한 충돌을 처리하기 위한 여러가지 방법을 제공합니다.
체이닝(Chaining) : 각 버킷에 연결 리스트를 사용하여 동일한 해시 값을 갖는 모든 요소를 저장합니다. 충돌이 발생하면 해당 버킷의 리스트에 요소를 추가합니다.
개방 주소법(Open Addressing) : 충돌이 발생하면 다른 빈 버킷을 찾아 데이터를 저장합니다. 이를 위해 다양한 탐사 방법(예: 선형 탐사, 제곱 탐사, 이중 해싱)을 사용합니다.
4️⃣ 해시 테이블의 시간 복잡도.
검색(Search) : O(1)(평균), O(n)(최악)
삽입(Insertion) : O(1)(평균), O(n)(최악)
삭제(Deletion) : O(1)(평균), O(n)(최악)
최악의 경우 시간 복잡도는 해시 충돌로 인해 모든 요소가 하나의 버킷에 저장될 때 발생합니다.
그러나, 좋은 해시 함수와 충돌 해결 방법을 사용하면 평균적으로 O(1)의 성능을 유지할 수 있습니다.
5️⃣ 해시 테이블의 장점과 단점.
장점
빠른 검색, 삽입, 삭제 성능(평균적으로 O(1))
키를 사용하여 데이터에 빠르게 접근 가능
단점
해시 함수의 성능에 의존
충돌 처이 필요
메모리 사용량이 증가할 수 있슴(특히 체이닝을 사용하는 경우)
💻 해시 테이블의 구현 예제.
아래는 Java에서 간단한 해시 테이블을 구현한 예제입니다.
// HashTable
import java.util.LinkedList;
class HashTable {
private class HashNode {
String key;
String value;
HashNode next;
public HashNode(String key, String value) {
this.key = key;
this.value = value;
}
}
private LinkedList<HashNode>[] buckets;
private int numBuckets;
private int size;
public HashTable() {
numBuckets = 10; // 버킷의 초기 크기
buckets = new LinkedList[numBuckets];
size = 0;
for (int i = 0; i < numBuckets; i++) {
buckets[i] = new LinkedList<>();
}
}
private int getBucketIndex(String key) {
int hashCode = key.hashCode();
int index = hashCode % numBuckets;
return index < 0 ? index * -1 : index;
}
public void put(String key, String value) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
for (HashNode node : bucket) {
if (node.key.equals(key)) {
node.value = value;
return;
}
}
bucket.add(new HashNode(key, value));
size++;
}
public String get(String key) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
for (HashNode node : bucket) {
if (node.key.equals(key)) {
return node.value;
}
}
return null;
}
public String remove(String key) {
int bucketIndex = getBucketIndex(key);
LinkedList<HashNode> bucket = buckets[bucketIndex];
HashNode prev = null;
for (HashNode node : bucket) {
if (node.key.equals(key)) {
if (prev != null) {
prev.next = node.next;
} else {
bucket.remove(node);
}
size--;
return node.value;
}
prev = node;
}
return null;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
// Main
public class Main {
public static void main(String[] args) {
HashTable hashTable = new HashTable();
hashTable.put("one", "1");
hashTable.put("two", "2");
hashTable.put("three", "3");
System.out.println("Value for key 'one': " + hashTable.get("one"));
System.out.println("Value for key 'two': " + hashTable.get("two"));
System.out.println("Removing key 'one': " + hashTable.remove("one"));
System.out.println("Contains key 'one': " + (hashTable.get("one") != null));
}
}
/*
출력
Value for key 'one': 1
Value for key 'two': 2
Removing key 'one': 1
Contains key 'one': false
*/
-
-
📦[DS,Algorithm] 해시(Hash)
1️⃣ 해시(Hash).
해시(Hash)란 컴퓨터 과학에서 주어진 입력 데이터를 고정된 크기의 고유한 값(일반적으로 숫자)으로 변환하는 과정 또는 그 결과 값을 말합니다.
해시는 주로 데이터 검색, 데이터 무결성 검증, 암호화 등에 사용됩니다.
1️⃣ 해시의 개념.
해시 함수(Hash Function)
임의의 길이를 가진 데이터를 고정된 길이의 해시 값으로 변환하는 함수입니다.
해시 함수는 동일한 입력에 대해 항상 동일한 해시 값을 생성해야 하며, 서로 다른 입력에 대해서는 가능한 한 다른 해시 값을 생성해야 합니다.
해시 값(Hash Value)
해시 함수를 통해 생성된 고정된 크기의 출력 값입니다.
이를 해시 코드(Hash Code) 또는 다이제스트(Digest)라고도 합니다.
2️⃣ 해시 함수의 특징.
결정성(Deterministic) : 동일한 입력에 대해 항상 동일한 해시 값을 반환합니다.
효율성(Efficiency) : 해시 함수는 입력 데이터를 빠르게 처리하여 해시 값을 생성해야 합니다.
충돌 저항성(Collision Resistance) : 서로 다른 두 입력이 동일한 해시 값을 갖지 않도록 해야 합니다. 현실적으로 완벽한 충돌 저항성은 불가능하므로, 가능한 충돌을 최소화하는 것이 중요합니다.
역상 저항성(Pre-image Resistance) : 해시 값을 통해 원해의 입력 데이터를 유추하는 것이 어렵거나 불가능해야 합니다.
두 번째 역상 저항성(Second Pre-image Resitance) : 특정 입력과 동일한 해시 값을 갖는 또 다른 입력을 찾는 또 다른 입력을 찾는 것이 어려워야 합니다.
3️⃣ 해시 함수의 용도.
데이터 검색 : 해시 테이블(Hash Table)과 같은 자료구조에서 빠른 데이터 검색을 위해 사용됩니다.
데이터 무결성 검증 : 데이터가 변경되지 않았음을 확인하기 위해 해시 값을 사용합니다. 예를 들어, 파일의 해시 값을 비교하여 파일이 손상되지 않았음을 확인할 수 있습니다.
암호화 및 보안 : 패스워드 저장, 디지털 서명, 메시지 인증 코드(MAC) 등에서 데이터의 무결성과 기밀성을 보장하기 위해서 사용됩니다.
4️⃣ 해시 함수의 예
SHA-256(Secure Hash Algorithm 256-bit) : 256비트의 해시 값을 생성하는 암호화 해시 함수입니다.
MD5(Message Digest Algorithm 5) : 128비트의 해시 값을 생성하는 해시 함수로, 현재는 충돌 저항성의 취약성 때문에 보안 용도로는 권장되지 않습니다.
CRC32(Cyclic Redundancy Check 32-bit) : 데이터 전송 오류 검출을 위해 사용되는 32비트 해시 함수입니다.
🙋♂️ 주요 포인트 요약
해시(Hash) 는 데이터를 고정된 크기의 고유한 값으로 변환하는 과정입니다.
해시 함수는 빠르고 효율적으로 해시 값을 생성하며, 충돌을 최소화하고 역상을 예측할 수 없도록 설계되어야 합니다.
해시 함수는 데이터 검색, 무결성 검증, 암호화 등 다양한 용도로 사용됩니다.
💻 해시 함수의 예제 코드
아래는 Java에서 SHA-256 해시 함수를 사용하여 문자열의 해시 값을 생성하는 예제입니다.
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
String input = "Hello World!";
try {
// SHA-256 해시 함수 인스턴스 생성
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 입력 문자열의 해시 값 계산
byte[] hash = digest.digest(input.getBytes());
// 해시 값을 16진수 문자열로 변환하여 출력
System.out.println("Hash value: " + bytesToHex(hash));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
// 바이트 배열을 16진수 문자열로 변환하는 함수
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}
-
-
-
-
-
💾 [CS] 다양한 보조기억장치
다양한 보조기억장치
보조기억장치에는 다양한 종류가 있습니다.
그중 가장 태중적인 보조기억장치는 하드 디스크와 플래시 메모리입니다.
우리가 흔히 사용하는 USB 메모리, SD 카드, SSD 같은 저장 장치를 말합니다.
하드 디스크(HDD: Hard Disk Drive)
하드 디스크(HDD: Hard Disk Drive) 는 자기적인 방식으로 데이터를 저장하는 보조기억장치입니다.
이 때문에 하드 디스크를 자기 디스크(magnetic disk) 의 일종으로 지칭하기도 합니다.
대용향 저장 장치가 필요한 작업이나 서버실에 자주 출입하는 작업을 한다면 하드 디스크를 자주 접하게 될 겁니다.
하드 디스크의 생김새.
다음 그림이 바로 하드 디스크입니다.
우리가 아는 CD나 옛날 음향 장치는 LP가 떠오를 겁니다.
실제로도 하드 디스크는 CD나 LP와 비슷하게 동작합니다.
동그란 원판에 데이터를 저장하고, 그것을 회전시켜 뾰족한 리더기로 데이터를 읽는 점에서 비슷합니다.
하드 디스크에서 실질적으로 데이터가 저장되는 곳은 아래 그림 속 동그란 원판입니다.
이를 플래터(platter) 라고 합니다.
하드 디스크는 자기적인 방식으로 데이터를 저장합니다.
플래터는 자기 물질로 덮여 있어 수많은 N극과 S극을 저장합니다.
N극과 S극은 0과 1의 역할을 수행합니다.
그 플래터를 회전시키는 구성 요소를 스핀들(spindle) 이라고 합니다.
스핀들이 플래터를 돌리는 속도는 분당 회전수를 나타내는 RPM(Revolution Per Minute) 이라는 단위로 표현됩니다.
가령 RPM이 15,000인 하드 디스크는 1분에 15,000바퀴를 회전하는 하드 디스크입니다.
플래터를 대상으로 데이터를 읽고 쓰는 구성 요소는 헤드(head) 입니다.
헤드는 플래터 위에서 미세하게 떠 있는 채로 데이터를 읽고 쓰는, 마치 바늘같이 생긴 부품입니다.
그리고 헤드는 원하는 위치로 헤드를 이동시키는 디스크 암(disk arm) 에 부착되어 있습니다.
CD나 LP에 비해 하드 디스크는 훨씬 더 많은 양의 데이터를 저장해야 하므로 일반적인 여러 겹의 플래터로 이루어져 있고 플래터 양면을 모두 사용할 수 있습니다.
양면 플래터를 사용하면 위아래로 플러터당 두 개의 헤드가 사용됩니다.
이 때 일반적으로 모든 헤드는 디스크 암에 부착되어 다같이 이동합니다.
데이터가 저장되는 방법.
그럼 이제 플래터에 데이터가 어떻게 저장되는지 알아봅시다.
플래터는 트랙(track) 과 섹터(sector) 라는 단위로 데이터를 저장합니다.
아래 그림터럼 플래터를 여러 동심원으로 나누었을 때 그중 하나의 원을 트랙이라고 부릅니다.
그리고 트랙은 마치 피자처럼 여러 조각으로 나우어지는데, 이 한 조각을 섹터라고 부릅니다.
섹터는 하드 디스크의 가장 작은 전송 단위입니다.
하나의 섹터는 일반적으로 512바이트 정도의 크기를 가지고 있지만, 정확한 크기는 하드 디스크에 따라 차이가 있습니다.
일부 하드 디스크의 섹터 크기는 4,096바이트에 이르기도 합니다.
여러 겹의 플래터가 사용 될 수 있습니다.
이때 여러 겹의 플래터 상에서 같은 트랙이 위치한 곳을 모아 연결한 논리적 단위를 실린더(cyilnder) 라고 부릅니다.
쉽게 말해 한 플래터를 동심원으로 나눈 공간은 트랙, 같은 트랙끼리 연결한 원통 모양의 공간은 실린더입니다.
연속된 정보는 보통 한 실린더에 기록됩니다.
예를 들어 두 개의 플래터를 사용하는 하드 디스크에서 네 개 섹터에 걸쳐 데이터를 저장할 때는 첫 번째 플래터 윗면, 뒷면과 두 번째 플래터 윗면, 뒷면에 데이터를 저장합니다.
연속된 정보를 하나의 실린더에 기록하는 이유는 디스크 암을 움직이지 않고도 바로 데이터에 접근할 수 있기 때문입니다.
데이터에 접근하는 과정
데이터가 하드 디스크의 섹터, 트랙, 실린더에 저장된다는 것을 알았다면 저장된 데이터에 접근하는 과정을 생각해 봅시다.
하드 디스크가 저장된 데이터에 접근하는 시간은 크게 탐색 시간, 회전 지연, 전송 시간 으로 나뉩니다.
탐색 시간(seek time) : 접근하려는 데이터가 저장된 트랙까지 헤드를 이동시키는 시간을 의미합니다.
회전 지연(rotational latency) : 헤드가 있는 곳으로 플래터를 회전시키는 시간을 의미합니다.
전송 시간(transfer time) : 하드 디스크와 컴퓨터 간에 데이터를 전송하는 시간을 의미합니다.
위 시간들은 별것 아닌 것 같아도 성능에 큰 영향을 끼치는 시간입니다.
일례로 구글의 AI를 주도하고 있는 제프 딘은 과거 ‘프로그래머가 꼭 알아야 할 컴퓨터 시간들’을 공개한 바 있는데, 일부를 발췌하면 다음과 같습니다.
물론 2011년에 자료가 공개된 이후 오늘날 하드 디스크 성능은 많이 향상되었지만, 하드 디스크에서 다량의 데이터를 탐색하고 읽어 들이는 시간은 생각보다 어마어마하다는 사실을 쉽게 짐작할 수 있습니다.
탐색 시간과 회전 지연을 단축시키기 위해서는 플래터를 빨리 돌려 RPM을 높이는 것도 중요하지만, 참조 지역성, 즉 접근하려는 데이터가 플래터 혹은 헤드를 조금만 옮겨도 접근할 수 있는 곳에 위치해 있는 것도 중요합니다.
플래시 메모리
하드 디스크는 최근에 많이 사용하는 보조기억장치이지만, 플래시 메모리(flush memory) 기반의 보조기억장치 또한 많이 사용합니다.
우리가 흔히 사용하는 USB 메모리, SD 카드, SSD가 모두 플래시 메모리 가반의 보조기억장치입니다.
플래시 메모리 내부.
다음 그림에서 붉은 박스로 표기한 부분이 플래시 메모리입니다.
플래시 메모리는 전기적으로 데이터를 읽고 쓸 수 있는 반도체 기반의 저장 장치입니다.
사실 플래시 메모리는 보조기억장치 범주에만 속한다기보다는 다양한 곳에서 널리 사용하는 저장 장치로 보는 것이 옳습니다.
주기억장치 중 하나인 ROM에도 사용되고, 우리가 일상적으로 접하는 거의 모든 전자 제품안에 플래시 메모리가 내장되어 있다고 봐도 무방합니다.
두 종류의 플래시 메모리
플래시 메모리에는 크래 NAND 플래시 메모리 와 NOR 플래시 메모리 가 있습니다.
NAND 플래시와 NOR 플래시는 각각 NAND 연산을 수행하는 회로(NAND 게이트)와 NOR 연산을 수행하는 회로(NOR 게이트)를 기반으로 만들어진 메모리를 뜻합니다.
이 둘 중 대용량 저장 장치로 많이 사용되는 플래시 메모리는 NAND 플래시 메모리 입니다.
플래시 메모리에는 셀(cell) 이라는 단위가 있습니다.
셀이란 플래시 메모리에서 데이터를 저장하는 가장 작은 단위입니다.
이 셀이 모이고 모여 MB, GB, TB 용량을 갖는 저장 장치가 되는 것입니다.
이 때 하나의 셀에 몇 비트를 저장할 수 있느냐에 따라 플래시 메모리 종류가 나뉩니다.
한 셀에 1비트를 저장할 수 있는 플래시 메모리를 SLC(Single Level Cell) 타입,
한 셀에 2비트를 저장할 수 있는 플래시 메모리를 MLC(Multiple Level Cell) 타입,
한 셀에 4비트를 저장할 수 있는 플래시 메모리를 TLC(Triple-Level Cell) 타입이라고 합니다.
큰 차이가 아닌 것처럼 보여도 이는 플래시 메모리의 수명, 속도, 가격에 큰 영향을 끼칩니다.
참고로 한 셀에 4비트를 저장할 수 있는 QLC 타입도 있습니다.
플래시 메모리도 수명이 있나요?
플래시 메모리에는 수명이 있습니다.
플래시 메모리 뿐만 아니라 하드 디스크 또한 수명이 있습니다.
우리가 사용하는 USB 메모리, SSD, SD 카드는 수명이 다하면 더 이상 저장 장치로써 사용이 불가능합니다.
종이에 연필로 쓰고 지우개로 지우고를 반복하다 보면 결국 종이가 찢어지는 것처럼 한 셀에 일정 횟수 이상 데이터를 쓰고 지우면
그 셀은 더 이상 데이터를 저장할 수 없기 때문입니다.
SLC, MLC, TCL 타입의 특징과 차이점.
사람 한 명을 비트, 셀을 집에 비유하면 SLC 타입은 한 집에 한 명, MLC 타입은 한 집에 두 명, TLC 타입은 세 명이 사는 구조로 비유할 수 있습니다.
SLC 타입
SLC 타입은 아래 그림과 같이 한 셀로 두 개의 정보를 표현할 수 있습니다.
홀로 거주하는 집에 제약 없이 출입이 가능하듯 SLC 타입은 MLC나 TLC 타입에 비해 비트의 빠른 입출력이 가능합니다.
수명도 MLC나 TLC 타입보다 길어서 수만에서 수십만 번 가까이 데이터를 쓰고 지우고를 반복할 수 있습니다.
하지만 SLC 타입은 용량 대비 가격이 높습니다.
이는 마치 혼자서 살면 감당해야 할 주거 비용이 커지는 것과 같습니다.
그렇기에 보통 기업에서 데이터를 읽고 쓰기가 매우 많이 반복되며 고성능의 빠른 저장 장치가 필요한 경우에 SLC 타입을 사용합니다.
MLC 타입
MLC 타입은 다음 그림과 같이 한 셀로 네 개의 정보를 표현할 수 있습니다.
SLC 타입보다 일반적으로 속도와 수명은 떨어지지만, 한 셀에 두 비트씩 저장할 수 있다는 점에서 MLC 타입은 SLC 타입도다 대용량화하기 유리합니다.
집의 개수가 같다면 한 집에 한 명씩 사는 것보다 한 집에 두 명씩 사는 것이 훨씬 더 많은 사람을 수용할 수 있는 것과 같은 이치입니다.
두 명이 한 집에서 주거 비용을 나눠 내면 혼자 감당해야 하는 주거 비용보다 저렴해지듯 MLC 타입은 SLC 타입보다 용량 대비 가격이 저렴합니다.
시중에서 사용되는 많은 플래시 메모리 저장 장치들이 MLC 타입(혹은 후술할 TLC 타입)으로 만들어집니다.
TLC 타입
한 셀당 3비트씩 저장할 수 있는 TLC 타입은 한 셀로 여덟 개의 정보를 표현할 수 있습니다.
그렇기에 대용화 하기 유리합니다.
일반적으로 SLC나 MLC 타입보다 수명과 속도가 떨어지지만 용량 대비 가격도 저렴합니다.
정리.
정리하면, 같은 용량의 플래시 메모리 저장 장치라고 할지라도 셀의 타입에 따라 수명, 가격, 성능이 다릅니다.
썻다 지우기를 자주 반복해야 하는 경우 혹은 높은 성능을 원하는 경우에는 고가의 SLC 타입을 선택하는 것이 좋고, 저가의 대용량 저장 장치를 원한다면 TLC 타입, 그 중간을 원한다면 MLC 타입의 저장 장치를 선택하는 것이 좋습니다.
플래시 메모리의 셀보다 더 큰 단위.
이제 플래시 메모리의 가장 작은 단위인 셀보다 더 큰 단위를 알아봅시다.
셀들이 모여 만들어진 단위를 페이지(page), 그리고 페이지가 모여 만들어진 단위를 블록(block) 이라고 합니다.
블록이 모여 플레인(plane), 플레인이 모여 다이(die) 가 됩니다.
플레시 메모리에서 읽기와 쓰기는 페이지 단위로 이루어 집니다.
하지만 삭제는 페이지보다 큰 블록 단위로 이루어집니다.
읽기/쓰기 단위와 삭제 단위가 다르다는 것이 플래시 메모리의 가장 큰 특징 중 하나입니다.
페이지의 상태.
페이지는 세 개의 상태를 가질 수 있습니다.
이는 각각 Free, Valid, Invalid 상태입니다.
Free 상태 : 어떠한 데이터도 저장하고 있지 않아 새로운 데이터를 저장할 수 있는 상태.
Valid 상태 : 이미 유효한 데이터를 저장하고 있는 상태.
Invalid 상태 : 쓰레기값이라 부르는 유효하지 않은 데이터를 저장하고 있는 상태.
플래시 메모리는 하드 디스크와는 달리 덮어쓰기가 불가능하여 Vaild 상태인 페이지에는 새 데이터를 저장할 수 없습니다.
플래시 메모리의 간단한 동작 예시.
플래시 메모리의 간단한 동작을 예시로 알아봅시다.
X라는 블록이 네 개의 페이지로 이루어져 있다고 가정해 보겠습니다.
그리고 그중 두 개의 페이지에는 왼쪽 아래와 같이 A와 B라는 데이터가 저장 되어 있다고 합시다.
여기서 블록 X에 새로운 데이터 C를 저장한다면 아래 그림과 같이 저장됩니다.
플래시 메모리의 읽기 쓰기 단위는 페이지이기 때문입니다.
여기서 새롭게 저장된 C와 기존에 저장되어 있던 B는 그대로 둔 채 기존의 A만을 A’로 수정하고 싶다면 플래시 메모리에서 덮어쓰기는 불가능하기 때문에 기존에 저장된 A는 Invalid 상태가 되어 더 이상 값이 유효하지 않은 쓰레기값이 되고, 새로운 A’ 데이터가 저장됩니다.
결과적으로 블록 X의 Valid 페이지는 B, C, A’가 됩니다.
그런데 여기서 문제가 있습니다.
A와 같이 쓰레기 값을 저장하고 있는 공간은 사용하지 않을 공간인데도 불구하고 용량을 차지하고 있습니다.
이는 엄연히 용량 낭비입니다.
그렇다고 A만 지울 수도 없습니다.
앞서 언급했듯이 플래시 메모리에서 삭제는 블록 단위로 수행되기 때문입니다.
그래서 최근 SSD를 비롯한 플래시 메모리는 이런 쓰레기 값을 정리하기 위해 가비지 컬렉션(Garbege Collection) 기능을 제공합니다.
가비지 컬렉션은 1. 유효한 페이지들만을 새로운 블록으로 복사한 뒤, 2. 기존의 블록을 삭제하는 기능입니다.
즉, 블록 X의 모든 유효한 페이지를 새로운 블록 T로 옮기고 블록 X를 삭제하는 것입니다.
키워드로 정리하는 핵심 포인트
하드 디스크 의 구성요소에는 플래터, 스핀들, 헤드, 디스크 암이 있습니다.
플래터는 트랙과 섹터로 나뉘고, 여러 플래터의 동일한 트랙이 모여 실린더를 이룹니다.
하드 디스크의 데이터 접근 시간은 크게 탐색 시간, 회전 지연, 전송 시간으로 나뉩니다.
플래시 메모리는 한 셀에 몇 비트를 저장할 수 있느냐에 따라 SLC, MLC, TLC로 나뉩니다.
플래시 메모리의 읽기과 쓰기는 페이지 단위로, 삭제는 블록 단위로 이루어 집니다.
-
-
💾 [CS] RAID의 정의와 종류
RAID의 정의와 종류.
1TB 하드 디스크 네 개로 RAID를 구성하면 4TB 하드 디스크 한 개의 성능과 안전성을 능가할 수 있습니다.
RAID의 정의.
‘보조기억장치에도 수명이 있습니다.’ 그래서 ‘하드 디스크와 같은 보조기억장치에 어떻게든 저장만 하면 됩니다’ 와 같은 단순한 답변은 다소 부족한 해법입니다.
이럴 때 사용할 수 있는 방법 중 하나가 RAID입니다.
RAID(Redundant Array of Independent Disks) 는 주로 하드 디스크와 SSD를 사용하는 기술로, 데이트의 안정선 혹은 높은 성능을 위해 여러 개의 물리적 보조기억장치를 마치 하나의 논리적 보조기억장치처럼 사용하는 기술을 의미합니다.
RAID의 종류
RAID 구성 방법을 RAID 레벨 이라고 표현합니다.
RAID 레벨에는 대표적으로 RAID 0, RAID 1, RAID 2, RAID 3, RAID 4, RAID 5, RAID 6 이 있고 그로부터 파생된 RAID 10, RAID 50 등이 있습니다.
RAID 0
RAID 0 은 여러 개의 보조기억장치에 데이터를 잔순히 나누어 저장하는 구성 방식입니다.
가령 1TB 하드 디스크 네 개로 RAID 0을 구성했다고 가정해 봅시다.
이제 어떠한 데이터를 저장할 때 각 하드 디스크는 아래와 같이 번갈아 가며 데이터를 저장합니다,
즉, 저장되는 데이터가 하드 디스크 개수만큼 나위어 자장되는 것입니다.
이때 마치 줄무늬처럼 분산되어 저장된 데이터를 스트라입(Stripe) 이라 하고, 분산하여 저장하는 것을 스트라이핑(Striping) 이라고 합니다.
위와 같이 데이터가 분산되어 저장되면, 다시 말해 스트라이핑되면 저장된 데이터를 읽고 쓰는 속도가 빨라집니다.
하나의 대용량 저장 장치를 이용했더라면 여러 번에 걸쳐 일고 썻을 데이터를 동시에 읽고 쓸 수 있기 때문입니다.
그렇기에 4TB 저장 장치 한 개를 읽고 쓰는 속도보다 RAID 0로 구성된 1TB 저장 장치 네 개의 속도가 이론상 네 배가량 빠릅니다.
RAID 0의 단점
RAID 0에는 단점이 있습니다.
저장된 정보가 안전하지 않습니다.
RAID 0으로 구성된 하드 디스크 중 하나에 문제가 생긴다면 다른 모든 하드 디스크의 정보를 읽는 데 문제가 생길 수 있습니다.
그래서 등장한 것이 RAID 1 입니다.
RAID 1
RAID 1 은 복사본을 만드는 방식입니다.
마치 거울처럼 완전한 복사본을 만드는 구성이기에 미러링(mirroring) 이라고도 부릅니다.
아래 그림은 네 개의 하드 디스크를 RAID 1으로 구성한 모습입니다.
RAID 0처럼 데이터 스트라이핑이 사용되긴 했지만, 오른쪽의 두 하드 디스크는 마치 거울처럼 왼쪽의 두 하드 디스크와 동일한 내용을 저장하고 있습니다.
이처럼 RAID 1에 어떠한 데이터를 쓸 때는 원본과 복사본 두 군데에 씁니다.
그렇기에 쓰기 속도는 RAID 0보다 느립니다.
RAID 1 방식은 복구가 매우 간단하다는 장점이 있습니다.
똑같은 디스크가 두 개 있는 셈이니, 하나에 문제가 발생해도 잃어버린 정보를 금방 되찾을 수 있기 때문입니다.
RAID 1의 단점
RAID 1은 하드 디스크 개수가 한정되었을 때 사용 가능한 용량이 적어지는 단점이 있습니다.
위 그림만 보아도 RAID 0 구성은 4TB의 정보를 저장할 수 있는 반면, RAID 1에서는 2TB의 정보만 저장할 수 있습니다.
즉, RAID 1에서는 복사본이 만들어지는 용량만큼 사용자가 사용하지 못합니다.
결국 많은 양의 하드 디스크가 필요하게 되고, 비용이 증가한다는 단점으로 이어집니다.
RAID 4
RAID 4는 RAID 1처럼 완전한 복사본을 만드는 대신 오류를 검출하고 복구하기 위한 정보를 저장한 장치를 두는 구성 방식입니다.
이때 ‘오류를 검출하고 복구하기 위한 정보’를 패리티 비트(parity bit) 라고 합니다.
RAID 4에서는 패리티를 저장한 장치를 이용해 다른 장치들의 오류를 검출하고, 오류가 있다면 복구합니다.
이로써 RAID 4는 RAID 1보다 적은 하드 디스크로도 데이터를 안전하게 보관할 수 있습니다.
오류를 검출하는 패리트 비트
원래 패리트 비트는 오류 검출만 가능할 뿐 오류 복구는 불가능합니다.
하지만 RAID에서는 패리트 값으로 오류 수정도 가능합니다.
다만 구체적인 방법인 패리티 계산법은 다루지 않을 예정입니다.
여기서 다음 두 가지만 기억하면 됩니다.
RAID 4에서는 패리티 정보를 저장한 장치로서 나머지 장치들의 오루를 검출.복구한다.
패리티 비트는 본래 오류 검출용 정보지만, RAID에서는 오류 복구도 가능하다.
RAID 5
RAID 4에서는 어떤 새로운 데이터가 저장될 때마다 패리티를 저장하는 디스크에도 데이터를 쓰게 되므로 패리티를 저장하는 장치에 병목 현상이 발생한다는 문제가 있습니다.
RAID 5는 아래 그림처럼 패리티 정보를 분산하여 저장하는 방식으로 RAID 4의 문제인 병목 현상을 해소합니다.
RAID 6
RAID 6 의 구성은 기본적으로 RAID 5와 같으나, 다음 그림과 같이 서로 다른 두 개의 패리티를 두는 방식입니다.
이는 오류를 검출하고 복구할 수 있는 수단이 두 개가 생긴 셈입니다.
따라서 RAID 6은 RAID 4나 RAID 5보다 안전한 구성이라 볼 수 있습니다.
다만 새로운 정보를 저장할 때마다 함께 저장할 패리티가 두 개이므로, 쓰기 속도는 RAID 5보다 느립니다.
따라서 RAID 6은 데이터 저장 속도를 조금 희생하더라도 데이터를 더욱 안전하게 보관하고 싶을 때 사용하는 방식입니다.
정리
이 외에도 RAID 0과 RAID 1을 혼합한 RAID 10 방식도 있고, RAID 0과 RAID 5를 혼합한 RAID 5방식도 있습니다.
note: 이렇게 여러 RAID 레벨을 혼합한 방식을 Nested RAID 라고 합니다.
각 RAID 레벨마다 장단점이 있으므로 어떤 상황에서 무엇을 최우선으로 원하는지에 따라 최적의 RAID 레벨은 달라질 수 있습니다.
그렇기에 각 RAID 레벨의 대략적인 구성과 특징을 아는것이 중요합니다.
키워드로 정리하는 핵심 포인트
RAID란 데이터의 안전성 혹은 높은 성능을 위해 여러 하드 디스크나 SSD를 마치 하나의 장치저럼 사용하는 기술입니다.
RAID 0은 데이터를 단순히 병렬로 분산하여 저장하고, RAID 1은 완전한 복사본을 만듭니다.
RAID 4는 패리티를 저장한 장치를 따로 두는 방식이고, RAID 5는 패리티를 분산하여 저장하는 방식입니다.
RAID 6은 서로 다른 두 개의 패리티를 두는 방식입니다.
-
📦[DS,Algorithm] 선형 자료구조 - 배열
1️⃣ 선형 자료구조 - 배열.
자료구조 관점에서 배열을 이해하고 여러 방법으로 구현 가능
1️⃣ 배열(Array).
자료구조 관점에서 배열(Array)은 동일한 타입의 데이터를 연속된 메모리 공간에 저장하는 선형 자료구조입니다.
배열은 조정된 크기를 가지며, 인덱스를 사용하여 각 요소에 빠르게 접근할 수 있는 특징이 있습니다.
배열은 가장 기본적이고 널리 사용되는 자료구조 중 하나입니다.
특징.
고정된 크기(Fixed Size)
배열은 선언 시 크기가 결정되며, 배열의 크기는 변경할 수 없습니다. 이 크기는 배열을 사용하는 동안 고정되어 있습니다.
예: ‘int[] numbers = new int[10];‘(크기가 10인 정수형 배열)
연속된 메모리 공간(Contiguous Memory Allocation)
배열의 요소들은 메모리상에 연속적으로 배치됩니다. 이는 인덱스를 통한 빠른 접근을 가능하게 합니다.
첫 번째 요소의 메모리 주소를 기준으로 인덱스를 사용하여 다른 요소의 주소를 계산할 수 있습니다.
인덱스를 통한 접근(Indexing)
배열의 각 요소는 인덱스를 통해 접근할 수 있습니다. 인덱스는 0부터 시작하여 배열의 크기 -1까지의 값을 가집니다.
예: ‘numbers[0]’,’numbers[1]‘,…,’numbers[9]‘
동일한 데이터 타입(Homogeneous Data Type)
배열은 동일한 데이터 타입의 요소들로 구성됩니다. 즉, 배열 내 모든 요소는 같은 데이터 타입이어야 합니다.
예: 정수형 배열, 문자열 배열 등.
장점.
빠른 접근 속도(Fast Access) :
인덱스를 사용하여 O(1) 시간 복잡도로 배열의 임의의 요소에 접근할 수 있습니다. 이는 배열의 주요 장점 중 하나입니다.
간단한 구현(Simple Implementation) :
배열은 데이터 구조가 간단하여 구현이 용이합니다. 기본적인 자료구조로, 다른 복잡한 자료구조의 기초가 됩니다.
단점.
고정된 크기(Fixed Size) :
배열의 크기는 선언 시 결정되며, 크기를 변경할 수 없습니다. 이는 크기를 사전에 정확히 예측하기 어려운 경우 비효율적일 수 있습니다.
삽입 및 삭제의 비효율성(Inefficient Insertions and Deletions) :
배열의 중간에 요소를 삽입하거나 삭제할 경우, 요소들을 이동시켜야 하기 때문에 O(n) 시간이 소요됩니다. 이는 큰 배열의 경우 성능 저하를 초래할 수 있습니다.
메모리 낭비(Memory Waste) :
배열의 크기를 너무 크게 설정하면 사용되지 않는 메모리가 낭비될 수 있고, 너무 작게 설정하면 충분한 데이터를 저장할 수 없습니다.
배열의 사용 예시.
정수형 배열 선언 및 초기화
int[] numbers = new int[5];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
배열의 요소 접근
int firstElement = numbers[0]; // 10
int lastElement = numbers[4]; // 50
배열의 순회
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
마무리.
배열은 다양한 상황에서 기본적인 데이터 저장과 접근 방법을 제공하며, 특정 요구사항에 맞춰 다른 자료구조와 함께 사용되기도 합니다.
배열의 빠른 접근 속도와 간단한 구조 덕분에, 많은 알고리즘과 프로그램에서 핵심적인 역할을 합니다.
2️⃣ 배열 직접 구현.
// CustomArray 클래스
public class CustomArray {
private int[] data;
private int size;
// 특정 용량으로 배열을 초기화하는 생성자
public CustomArray(int capacity) {
data = new int[capacity];
size = 0;
}
// 배열의 크기를 가져오는 메서드
public int size() {
return size;
}
// 배열이 비어 있는지 확인하는 메서드
public boolean isEmpty() {
return size == 0;
}
// 특정 인덱스의 요소를 가져오는 메서드
public int get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
return data[index];
}
// 특정 인덱스에 요소를 설정하는 메서드
public void set(int index, int value) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
data[index] = value;
}
// 배열에 요소를 추가하는 메서드
public void add(int value) {
if (size == data.length) {
throw new IllegalStateException("Array is full");
}
data[size] = value;
size++;
}
// 특정 인덱스의 요소를 삭제하는 메서드
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
}
// 모든 요소를 출력하는 메서드
public void print() {
for (int i = 0; i < size; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
}
설명.
필드:
‘data’ : 실제 데이터를 저장하는 배열.
‘size’ : 현재 배열에 저장된 요소의 개수.
생성자:
‘CustomArray(int capacity)’ : 초기 용량을 설정하여 배열을 초기화 합니다.
메서드:
‘size()’ : 현재 배열에 저장된 요소의 개수를 반환합니다.
‘isEmpty()’ : 배열이 비어있는지 확인합니다.
‘get(int index)’ : 특정 인덱스의 요소를 반환합니다.
‘set(int index, int value)’ : 특정 인덱스의 요소를 설정합니다.
‘add(int value)’ : 배열의 마지막에 요소를 추가합니다.
‘remove(int index)’ : 특정 인덱스의 요소를 제거하고, 이후의 요소들을 앞으로 이동시킵니다.
‘print()’ : 배열의 모든 요소를 출력합니다.
-
📦[DS,Algorithm] 자료구조 소개
1️⃣ 자료구조(Data Structure)
자료구조(Data Structure)는 데이터를 효율적으로 저장하고 관리하며, 이를 통해 데이터를 효율적으로 접근하고 수정할 수 있도록 하는 체계적인 방법입니다.
자료구조는 알고리즘의 성능을 최적화하고 프로그램의 효율성을 향상시키는 데 중요한 역할을 합니다.
기본 개념.
자료구조는 데이터를 저장하는 방식과 데이터를 조작하는 방법을 정의합니다.
이는 데이터를 어떻게 배열하고, 접근하고, 수정하고, 삭제할지를 규정하는 규칙과 방법의 집합입니다.
주요 목적.
효율적인 데이터 저장 및 접근.
데이터를 효율적으로 저장하여 빠르게 접근하고 검색할 수 있도록 합니다.
데이터 수정 및 삭제 용이.
데이터를 쉽게 수정하고 삭제할 수 있도록 합니다.
알고리즘 최적화.
알고리즘의 성능을 최적화하고 실행 시간을 단축시킵니다.
주요 종류.
배열(Array)
고정된 크기의 연속된 메모리 공간에 데이터를 저장합니다.
인덱스를 사용하여 데이터에 빠르게 접근할 수 있습니다.
연결 리스트(Linked List)
각 요소가 데이터와 다음 요소를 가리키는 포인터를 포함합니다.
동적 크기 조절이 가능하며 삽입과 삭제가 용이합니다.
스택(Stack)
후입선출(LIFO, Last In First Out) 방식으로 동작합니다.
데이터를 삽입하는 push와 삭제하는 pop 연산을 가집니다.
큐(Queue)
선입선출(FIFO, First In First Out) 방식으로 동작합니다.
데이터를 삽입하는 enqueue와 삭제하는 dequeue 연산을 가집니다.
트리(Tree)
계층적 구조를 가지며, 노드와 에지로 구성됩니다.
이진 트리, 이진 탐색 트리, AVL 트리 등 다양한 형태가 있습니다.
그래프(Graph)
노드(정점)와 에지(간선)로 구성된 자료구조로, 다양한 관계를 표현할 수 있습니다.
방향 그래프, 무방향 그래프 등이 있습니다.
해시 테이블(Hash Table)
키-값 쌍을 저장하며, 해시 함수를 사용하여 데이터에 빠르게 접근할 수 있습니다.
충돌 해결 방법으로 체이닝과 개방 주소법이 있습니다.
응용 사례.
자료구조는 데이터베이스, 운영체제, 네트워크, 인공지능, 게임 개발 등 다양한 분야에서 중요한 역할을 합니다.
적절한 자료구조의 선택은 프로그램의 성능과 효율성을 크게 향상시킬 수 있습니다.
2️⃣ 자료구조의 분류
1️⃣ 선형 자료구조(Linear Data Structure)
선형 자료구조(Linear Data Structure)는 데이터 요소들이 순차적으로 배열된 구조를 의미합니다.
이 자료구조에서는 데이터 요소들이 직선 형태로 연결되어 있으며, 각 요소는 한 다음 요소나 이전 요소와 연결되어 있습니다.
선형 자료구조의 주요 특징인 데이터 요소들이 한 줄로 배열되어 있다는 점 입니다.
주요 선형 자료구조르는 배열, 연결 리스트, 스택, 큐 등이 있습니다.
주요 선형 자료구조.
배열(Array)
정의 : 동일한 타입의 데이터 요소들이 연속된 메모리 공간에 저장되는 자료구조입니다.
특징 : 고정된 크기를 가지며 인덱스를 통해 데이터에 빠르게 접근할 수 있습니다.
예시 : 정수형 배열, 문자열 배열 등.
연결 리스트(Linked List)
정의 : 각 데이터 요소가 노드로 구성되고, 각 노드는 데이터와 다음 노드를 가리키는 포인터를 포함하는 자료구조입니다.
특징 : 동적 크기 조절이 가능하며 삽입과 삭제가 용이하지만, 인덱스를 통한 접근은 배열보다 느립니다.
종류 : 단일 연결 리스트, 이중 연결 리스트, 원형 연결 리스트 등.
스택(Stack)
정의 : 후입선출(LIFO, Last In First Out) 방식으로 동작하는 자료구조입니다.
특징 : 데이터 삽입(push)과 삭제(pop)이 한쪽 끝에서만 이루어집니다.
사용 사례 : 함수 호출 스택, 역순 문자열 처리 등.
큐(Queue)
정의 : 선입선출(FIFO, First In First Out) 방식으로 동작하는 자료구조입니다.
특징 : 데이터의 삽입(enqueue)은 한쪽 끝(후단)에서, 삭제(dequeue)는 반대쪽 끝(전단)에서 이루어집니다.
종류 : 원형 큐, 우선순위 큐, 덱(Deque) 등.
사용 사례 : 운영 체제의 작업 스케줄링, 프린터 대기열 등.
선형 자료구조의 특징 및 장단점.
특징.
순차적 접근이 가능하며, 데이터를 차례대로 처리할 때 유리합니다.
메모리에서 연속적으로 배치되므로 인덱스를 통해 직접 접근할 수 있습니다.(배열의 경우)
장점.
간단하고 구현이 용이합니다.
데이터의 삽입과 삭제가 특정 조건 하에 효율적일 수 있습니다(예: 스택, 큐)
단점
데이터 크기에 따라 메모리 낭비가 발생할 수 있습니다(배열의 경우).
특정 요소 접근이나 삽입/삭제 시 성능이 저하될 수 있습니다(연결 리스트의 경우)
마무리.
선형 자료구조는 데이터가 순차적으로 연결되어 있어 순차적 처리에 적합하며, 프로그램의 다양한 부분에서 사용되는 기초적인 자료구조입니다.
2️⃣ 비선형 자료구조(Non-linear Data Structure)
비선형 자료구조(Non-linear Data Structure)는 데이터 요소들이 계층적 또는 그물 형태로 배열된 구조를 의미합니다.
이 자료구조에서는 데이터 요소들이 순차적으로 배열되지 않고, 하나의 요소가 여러 요소들과 연결될 수 있습니다.
주요 비선형 자료구조로는 트리(Tree)와 그래프(Graph)가 있습니다.
주요 비선형 자료구조.
트리(Tree)
정의 : 노드와 그 노드들을 연결하는 간선으로 구성된 계층적 자료구조입니다. 트리는 루트 노드에서 시작하여 자식 노드로 분기하며, 사이클이 없습니다.
특징 : 트리는 계층적 관계를 나타내며, 각 노드는 0개 이상의 자식 노드를 가질 수 있습니다.
종류 :
이진 트리(Binary Tree) : 각 노드가 최대 두 개의 자식 노드를 가지는 트리입니다.
이진 탐색 트리(Binary Search Tree) : 왼쪽 자식은 부모보다 작고, 오른쪽 자식은 부모보다 큰 값을 가지는 이진 트리입니다.
균형 이진 트리(Balanced Binary Tree) : AVL 트리, 레드-블랙 트리 등과 같이 높이가 균형을 이루도록 유지되는 트리입니다.
힙(Heap) : 완전 이진 트리의 일종으로, 최대 힙과 최소 힙이 있습니다.
트라이(Trie) : 문자열을 저장하고 빠르게 검색하기 위해 사용되는 트리입니다.
그래프(Graph)
정의 : 정점(Vertex)들과 이 정점들을 연결하는 간선(Edge)들로 구성된 자료구조입니다.
특징 : 그래프는 방향 그래프(Directed Graph)와 무방향 그래프(Undirceted Graph)로 나눌 수 있으며, 사이클이 존재할 수 있습니다.
종류 :
방향 그래프(Directed Graph) : 간선에 방향성이 있는 그래프입니다.
무방향 그래프(Undirected Graph) : 간선에 방향성이 없는 그래프입니다.
가중치 그래프(Weighted Graph) : 간선에 가중치가 부여된 그래프입니다.
비가중치 그래프(Unweighted Graph) : 간선에 가중치가 없는 그래프입니다.
비선형 자료구조의 특징 및 장단점.
특징 :
계층적 또는 네트워크 구조를 나태내는 데 적합합니다.
복잡한 관계를 표현할 수 있으며, 데이터 요소 간의 다대다 관계를 처리할 수 있습니다.
장점 :
데이터의 계층적 구조를 쉽게 표현할 수 있습니다(트리).
복잡한 연결 관계를 효과적으로 모델링할 수 있습니다(그래프).
특정 유형의 탐색, 정렬, 데이터 압축, 네트워크 라우팅 등에 유용합니다.
단점 :
구현과 관리가 선형 자료구조보다 복잡할 수 있습니다.
특정 작업(예: 트리의 균형 유지, 그래프 탐색 등)에서 추가적인 알고리즘이 필요합니다.
마무리.
비선형 자료구조는 데이터가 단순히 순차적으로 배열되지 않고, 복잡한 관계를 나타내는 경우에 사용됩니다.
예를 들어, 파일 시스템의 디렉터리 구조, 데이터베이스 인덱스, 소셜 네트워크의 사용자 관계 등이 비선형 자료구조를 활용하는 사례입니다.
2️⃣ 자료구조의 구현.
1️⃣ 추상 자료형(Abstract Data Type, ADT)
자바 프로그래밍에서의 추상 자료형(Abstract Data Type, ADT)은 데이터의 논리적 구조와 이를 조작하는 연산들을 명확하게 정의한 개념입니다.
ADT는 구현 세부 사항을 숨기고, 데이터와 연산의 인터페이스를 통해 사용자에게 추상적인 수준에서 데이터 조작을 제공합니다.
즉, ADT는 데이터가 어떻게 저장되고 구현되는지에 대한 정보는 감추고, 데이터와 상호작용하는 방법만을 정의합니다.
주요 개념
추상화(Abstraction)
ADT는 데이터를 추상화하여 데이터의 실제 구현과 독립적으로 사용될 수 있도록 합니다.
사용자는 데이터의 저장 방식이나 연산의 구현 방법을 알 필요 없이, ADT가 제공하는 인터페이스를 통해 데이터를 조작할 수 있습니다.
인터페이스(Interface)
ADT는 데이터 타입과 이를 다루는 연산들을 인터페이스를 통해 정의합니다.
자바에서는 인터페이스(Interface) 키워드를 사용하여 ADT의 연산을 정의할 수 있습니다.
캡슐화(Encapsulation)
ADT는 데이터와 연산을 하나의 단위로 묶어 캡슐화합니다.
데이터를 직접 접근하지 않고, 정의된 연산을 통해서만 접근할 수 있도록 하여 데이터 무결성을 보장합니다.
자바에서의 ADT 예시
다음은 자바에서 스택(Stack) ADT를 정의하고 구현하는 예시입니다.
스택 인터페이스 정의
public interface Stack<T> {
void push(T item); // 스택에 아이템을 추가
T pop(); // 스택에서 아이템을 제거하고 반환
T peek(); // 스택의 맨 위 아이템을 반환(제거하지 않음)
boolean isEmpty(); // 스택이 비어 있는지 확인
int size(); // 스택의 크기 반환
}
스택 구현
import java.util.ArrayList;
import java.util.List;
public class ArrayListStack<T> implements Stack<T> {
private List<T> list = new ArrayList<>();
@Override
public void push(T item) {
list.add(item);
}
@Override
public T pop() {
if (isEmpty()) {
throw new RuntimException("Stack is empty");
}
return list.remove(list.size() - 1);
}
@Override
public T peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return list.get(list.size() - 1);
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public int size() {
return list.size();
}
}
설명
‘Stack<T>‘ 인터페이스는 스택 ADT의 연산을 정의합니다. 이 인터페이스는 ‘push’, ‘pop’, ‘peek’, ‘isEmpty’, ‘size’ 메서드를 포함합니다.
‘ArrayListStack<T>‘ 클래스는 ‘Stack<T>‘ 인터페이스를 구현합니다. 이 클래스는 ‘ArrayList’ 를 내부 데이터 구조로 사용하여 스택 연산을 구현합니다.
‘push’ 메서드는 스택에 아이템을 추가합니다.
‘pop’ 메서드는 스택에서 맨 위의 아이템을 제거하고 반환합니다.
‘peek’ 메서드는 스택의 맨 위 아이템을 제거하지 않고 반환합니다.
‘isEmpty’ 메서드는 스택이 비어 있는지 확인합니다.
‘size’ 메서드는 스택의 크기를 반환합니다.
이 예시에서 ‘Stack<T>‘ 인터페이스는 스택 ADT를 정의하고, ‘ArrayListStack<T>‘ 클래스는 이 ADT를 구현한 것입니다.
사용자는 ‘ArrayListStack’ 의 내부 구현을 알 필요 없이 ‘Stack’ 인터페이스를 통해 스택 연산을 사용할 수 있습니다.
이는 ADT의 주요 장점 중 하나인 구현의 독립성을 잘 보여줍니다.
-
-
-
-
-
-
📝[blog post] 연습 문제 풀이 정리(2)
1️⃣ 수열과 재귀.
연습 문제를 풀다보니 수열과 재귀에 대해 많은 수학적 사고력이 필요하겠다는 생각이 들었습니다.
수열 : 수학에서 수의 나열을 의미합니다.
즉, 어떤 규착에 따라 나열된 수들의 집합을 말합니다.
수열은 각 수를 나타내는 일련의 할(terms)으로 구성되며, 각 항은 특정 위치(index)를 가집니다.
수열의 예로는 다음과 같은 것들이 있습니다.
등차수열: 각 항이 일정한 값만큼 증가하거나 감소하는 수열(예: 2, 5, 8, 11…)(각 항이 3씩 증가)
등비수열: 각 항이 일정한 비율로 증가하거나 감소하는 수열(예: 3, 9, 27, 81…)(각 항이 이전 항의 3배)
피보나치 수열: 첫 두 항이 0과 1이고, 그 이후의 각 항이 바로 앞 두항의 합인 수열(예: 0, 1, 1, 2, 3, 5, 8….)
수열은 다양한 수학적 문제를 해결하는 데 사용되며, 특히 함수, 극한, 미적분 등의 주제와 밀접한 관련이 있습니다.
재귀 : 프로그래밍과 수학에서 사용되는 개념으로, 어떤 함수나 알고리즘이 자기 자신을 호출하는 방식울 말합니다.
재귀를 통해 복잡한 문제를 더 작은 하위 문제로 나누어 해결할 수 있습니다.
재귀 함수는 기본적으로 두 가지 부분으로 구성됩니다.
1. 기저 조건(Base Case) : 재귀 호출이 더 이상 필요하지 않은 경우를 정의합니다. 기저 조건이 충족되면 함수는 더 이상 자기 자신을 호출하지 않고 종료됩니다.
2. 재귀 호출(Recursive Call) : 함수가 자기 자신을 호출하여 문제를 더 작은 부분으로 나누어 해결하려고 시도합니다.
재귀는 문제를 단순하고 직관적으로 표현할 수 있는 강력한 도구이지만, 재귀 호출이 과도하면 스택 오버플로(stack overflow)가 발생할 수 있으므로 주의가 필요합니다.
따라서 재귀를 사용할 때는 기저 조건을 잘 정의하고, 필요할 경우 반복(iteration)으로 문제를 해결하는 방법도 고려해야 합니다.
-
-
📝[blog post] 연습 문제 풀이 정리(1)
1️⃣ 이중 for 문.
이중 for 문은 for 문을 중첩해서 사용하는 것을 말합니다.
한 for 문 안에 또 다른 for 문 안에 또 다른 for 문이 들어있는 구조로, 주로 2차원 배열이나 리스트, 행렬을 처리할 때 사용됩니다.
1.1 기본 구조.
for (초기화1; 조건1; 증감1) {
for (초기화2; 조건2; 증감2) {
// 코드 블록
}
}
1.2 예시
예를 들어, 2차원 리스트의 모든 요소를 출력하는 경우를 생각해 봅시다.
public class Main {
public static void main(Stringp[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int[] row : matrix) {
for (int element : row) {
System.out.println(element);
}
}
}
}
위 코드에서 ‘matrix’ 는 2차원 리스트입니다.
첫 번째 for 문은 ‘matrix’ 의 각 행(row)을 순회하고, 두 번째 for 문은 각행의 요소(element)를 순회합니다.
출력 결과는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
2️⃣ 규칙성을 찾는 것이 중요!
어떤 문제를 마주치면 규칙성을 찾는 것이 중요한 것 같습니다.
연습 문제 2-1 중 ‘정수형 숫자를 로마 숫자 표기로 변환하는 프로그램’ 을 작성하는 문제에서 그것을 깨달았습니다.
먼저 어떤 규칙성이 있는지 찾아낸 후 그 규칙성에 따라 문제를 풀고, 문제를 컴퓨터적 사고력을 이용하여 코딩을 하니 문제가 풀리는 것을 알게 되었습니다.
3️⃣ 인덱스를 자유자재로 가지고 놀 줄 알아야 합니다!
연습 문제를 풀면서 느낀 점 중 하나가 “인덱스를 자유자재로 가지고 놀 줄 알아야 한다” 는 부분이었습니다.
“인덱스를 자유자재로 가지고 논다” 라는 말은 문자열이 주어지면 인덱스를 활용하여 문자를 삽입, 삭제, 추출, 변환 등을 자유롭게 할 줄 알아야 한다는 의미입니다.
연습 문제 중 문자열에 대한 문제는 이 부분이 가장 중요시되는 것 같았습니다.
-
☕️[Java] 스트림
1️⃣ 스트림.
1. 스트림(Stream)
자바에서 스트림(Stream) API는 자바 8에서 도입되어 컬렉션의 요소를 선언적으로 처리할 수 있는 방법을 제공합니다.
스트림 API를 이용하면 데이터 요소의 시퀀스를 효율적으로 처리할 수 있으며, 데이터를 병렬로 처리하는 것도 간단할게 할 수 있습니다.
스트림을 이용하면 복잡한 데이터 처리 작업을 간결하고 명확한 코드로 작성할 수 있습니다.
1.2 스트림의 주요 특정.
1. 선언적 처리 : 스트림을 사용하면 무엇을 할 것인지(what)에 집중하여 작업을 설명할 수 있고, 어떻게 처리할 것인지(how)는 스트림 API가 알아서 최적화하여 처리합니다.
2. 파이프라이닝 : 스트림 연산은 파이프라인을 형성할 수 있으며, 여러 단계의 처리 과정을 연결하여 복잡한 데이터 처리를 효과적으로 할 수 있습니다.
3. 내부 반복 : 스트림은 “내부 반복”을 사용합니다. 즉, 데이터를 어떻게 반복할지 스트림이 처리하므로, 개발자는 각 요소에 어떤 처리를 할지만 정의하면 됩니다.
4. 불변성 : 스트림은 데이터를 수정하지 않습니다. 대신, 각 단계에서 결과를 내는 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 특성을 반영합니다.
1.3 스트림의 작업 흐름.
스트림 API의 작업 흐름은 크게 세 부분으로 나눌 수 있습니다.
1. 스트림 생성 : 컬렉션, 배열, I/O 자원 등의 데이터 소스로부터 스트림을 생성합니다.
List<String> myList = Arrays.asList("a1", "a2", "b1", "b2", "c2", "c1");
Stream<String> myStrean = myList.stream();
2. 중간 연산(Intermediate operations) : 스트림을 변환하는 연산으로, 필터링, 매핑, 정렬 등이 있으며, 이 연산들은 연결 가능하고, 또한 게으르게(lazily) 실행됩니다.
myStream.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted();
3. 종단 연산(Terminal operations) : 스트림의 요소들을 소모하여 결과를 생성하는 연산입니다. 예를 들어, forEach, reduce, collect 등이 있으며, 이 연산을 수행한 후 스트림은 더 이상 사용할 수 없습니다.
myStream.forEach(System.out::println);
1.4 스트림과 병렬 처리.
스트림 API는 병렬 처리를 간단하게 지원합니다.
‘paralleStream()’ 을 호출하면 자동으로 여러 쓰레드에서 스트림 연산이 병렬로 수행됩니다.
이는 데이터가 큰 경우에 유용하며, 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 해줍니다.
1.5 📝 정리.
스트림은 자바에서 데이터 컬렉션을 함수형 스타일로 쉽게 처리할 수 있게 하는 강력한 도구입니다.
이는 코드의 간결성과 가독성을 높이는 데 큰 도움을 줍니다.
2. 중개 연산(Intermediate operations)
자바 스트림 API에서 중개 연산(Intermediate operations)은 스트림의 요소들을 처리하고 변형하는 연산들로서, 다른 스트림을 반환합니다.
중개 연산은 게으른(lazy) 특성을 가지며, 종단 연산(Terminal operation)이 호출되기 전까지는 실제로 실행되지 않습니다.
이런 특성은 연산의 체인을 구성할 때 성능 최적화에 도움을 줍니다.
2.1 중개 연산의 주요 특성.
게으른 실행(Lazy Execution) : 중개 연산은 호출되었을 때 즉시 실행되지 않습니다. 대신, 종단 연산이 호출될 때 까지 실행이 지연됩니다.
스트림 변환 : 각 중개 연산은 변형된 형태의 새로운 스트림을 변환합니다. 이는 연산을 연쇄적으로 연결할 수 있도록 합니다.
2.2 주요 중개 연산의 종류.
1. 필터링(Filtering)
‘filter(Predicate<T> predicate)’ : 주어진 조건(프리디케이트)에 맞는 요소만을 포함하는 스트림을 반환합니다.
List<String> names = Arrays.asList("Jo", "Lee", "Park", "Kang");
names.stream()
.filter(name -> name.startsWith("K"))
.forEach(System.out::println); // 출력: "Kang"
2. 매핑(Mapping)
‘map(Function<T, R> mapper)’ : 스트림의 각 요소에 주어진 함수를 적용하고, 함수 결과로 주성된 새 스트림을 반환합니다.
‘flatMap(Function<T, Stream<R>> mapper)’ : 각 요소에 함수를 적용한 결과로 생성된 여러 스트림을 하나의 스트림으로 평탄화합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.stream()
.map(number -> number * number)
.forEach(System.out::println); //출력: 1, 4, 9, 16
3. 정렬(Sorting)
‘sorted() :’ 자연 순서대로 스트림을 정렬합니다.
‘sorted(Comparator<T> comparator) :’ 주어진 비교자를 사용하여 스트림을 정렬합니다.
List<String> fruits = Arrays.asList("banana", "apple", "orange", "kiwi");
fruits.stream()
.sorted()
.forEach(System.out::println); // 출력: apple, banana, kiwi, orange
4. 제한(Limiting) 및 건너뛰기(Skipping)
‘limit(long maxSize)’ : 스트림의 요소를 주어진 크기로 제한합니다.
‘skip(long n)’ : 스트림의 처음 n개 요소를 건너뜁니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.skip(2)
.limit(3)
.forEach(System.out::println); // 출력 3, 4, 5
5. 중복 제거(Distinct)
‘distinct()’ : 스트림에서 중복된 요소를 제거합니다.
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
numbers.stream()
.distinct()
.forEach(System.out::println); // 출력: 1, 2, 3, 4, 5
2.3 📝 정리.
중개 연산을 통해 데이터 스트림을 세밀하게 제어하고 원하는 형태로 데이터를 변형 할 수 있습니다.
이러한 연산들은 다양한 데이터 처리 작업에서 매우 유용하게 사용됩니다.
3. 최종 연산(Terminal operations)
자바 스트림 API에서 최종 연산(Terminal operations)은 스트림 파이프라인의 실행을 트리거하고 스트림의 요소를 소비하여 결과를 생성하거나 부작용(side effect)을 일으키는 연산입니다.
최종 연산이 호출되기 전까지 중간 연산들은 게으른(lazy) 방식으로 처리되며 실행되지 않습니다.
최종 연산 후에는 스트림이 소비되어 더 이상 사용할 수 없게 됩니다.
3.1 최종 연산의 주요 유형.
1. 수집(Collection)
‘collect(Collector<T, A, R> collector)’ : 스트림의 요소를 변환, 결합하고 컬렉션으로 또는 다른 형태로 결과를 수집합니다.
예를 들어, ‘toList()’, ‘toSet()’, ‘toMap()’ 등이 있습니다.
List<String> names = Array.asList("Alice", "Bob", "Charlie", "David");
List<String> list = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(list); // 출력 ["Alice"]
2. 집계(Aggregation)
‘count()’ : 스트림의 요소 개수를 반환합니다.
‘max(Comparator<T> comparator)’ : 스트림에서 최대값을 찾습니다.
‘min(Comparator<T> comparator)’ : 스트림에서 최소값을 찾습니다.
‘reduce(BinaryOperator<T> accumulator)’ : 스트림의 요소를 결합하여 하나의 결과를 생성합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 출력: 10
3. 반복(Iteration)
‘forEach(Consumer<T> action)’ : 각 요소에 대해 주어진 작업을 수행합니다. 스트림의 순서대로 실행됩니다.
List<String> names = Arrays.asList("Alist", "Bob", "Charlie", "David");
names.stream()
.forEach(System.out::println); // Alice, Bob, Charlie, David
4. 조건 검사(Checking)
‘allMatch(Predicate<T> predicate) :’ 모든 요소가 주어진 조건을 만족하는지 검사합니다.
‘anyMatch(Predicate<T>predicate) :’ 어떤 요소라도 주어진 조건을 만족하는지 검사합니다.
‘noneMatch(Predicate<T>predicate) :’ 모든 요소가 주어진 조건을 만족하지 않는지 검사합니다.
boolean allEven = numbers.stream()
.allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 출력: false
5. 요소 검색(Finding)
‘findFirst()’ : 스트림의 첫 번째 요소를 Optional로 반환합니다.
‘findAny()’ : 스트림에서 임의의 요소를 Optional로 반환합니다. 병렬 스트림에서 유용합니다.
Optional<String> first = names.stream()
.findFirst();
first.ifPresent(System.out::println); // 출력 Alice
3.2 📝 정리.
이러한 최종 연산들은 스트림 처리를 완료하고 필요한 결과를 도출하기 위해 사용됩니다.
스트림 API를 통해 데이터 처리를 선언적이고 간결하게 할 수 있으며, 복잡한 로직을 효과적으로 관리할 수 있습니다.
-
-
☕️[Java] 람다식은 하나만!
람다식은 하나만!😆
자바에서는 “하나의 추상 메소드를 갖는 인터페에스에 대해서만 람다식을 직접 사용할 수 있습니다.”
이를 함수형 인터페이스라고 부르며, 람다식은 이런 함수형 인터페이스의 구현을 간단히 할 수 있는 방법을 제공합니다.
하지만 아래의 코드와 같이 인터페이스 내에 두 개의 추상 메서드 (‘plus’, ‘minus’)가 있기 때문에, 이 인터페이스를 람다식으로 직접 구현하는 것은 불가능합니다.
interface Carculator {
public abstract int plus(int x, int y);
public abstract int minus(int x, int y);
}
람다식을 사용하려면 함수형 인터페이스가 필요하므로, 두 메소드 각각을 위한 두 개의 별도의 인터페이스를 정의하거나 기존 인터페이스 중 하나를 수정해야 합니다.
아래의 코드는 이를 위해 각 메소드를 분리하여 두 개의 함수형 인터페이스를 만든 예시입니다.
interface Calculator {
public abstract int operation(int x, int y);
}
public class Main {
public static void main(String[] args) {
Calculator plus = (x, y) -> { return x + y; };
System.out.println(plus.operation(10,2)); // 12
Calculator minus = (x, y) -> { return x - y; };
System.out.println(minus.operation(10,2)); // 8
}
}
위 코드는 각 연산을 람다식으로 간단히 구현하고 있습니다.
만약 원래의 ‘Carculator’ 인터페이스를 유지고하고 싶다면 이를 직접적으로 람다식으로 구현할 수는 없으며, 대신 익명 클래스나 정규 클래스를 사용해야 합니다.
아래의 코드는 익명 클래스를 사용하는 방법을 보여줍니다.
Calculator calclator = new Calculator() {
@Override
public int plus(int x, int y) {
return x + y;
}
@Override
public int minus(int x, int y) {
return x - y;
}
}
-
☕️[Java] 람다식
1️⃣ 람다식.
1. 람다 표현식(Lambda Expression)
자바 프로그래밍에서 람다식 또는 람다 표현식(Lambda Expression)은 간결한 방식으로 익명 함수(anonymous function)를 제공하는 기능입니다.
자바 8부터 도입된 이 기능은 함수형 프로그래밍의 일부 개념을 자바에 도입하여, 코드를 더 간결하고 명료하게 만들어 주며 특히 컬렉션의 요소를 처리할 때 유용하게 사용됩니다.
1.2 람다식의 특징.
익명성 : 람다는 이름이 없기 때문에 익명으로 처리됩니다.
함수 : 람다는 메서드와 유사하지만, 독립적으로 존재할 수 있는 함수입니다.
전달성 : 람다 표현식은 메서드 인자로 전달되거나 변수에 저장될 수 있습니다.
간결성 : 코드의 간결성을 높여, 불필요한 반복을 줄여줍니다.
1.3 람다 표현식의 기본 구조.
람타 표현식은 주로 매개 변수를 받아들여 결과를 반환하는 식의 형태로 작성됩니다.
일반적인 형태는 다음과 같습니다.
(parameters) -> expression
또는
(parameters) -> { statements; }
매개 변수 : 괄호 안에 정의되며, 매개 변수의 타입을 명시할 수도 있고 생략할 수도 있습니다.
매개 변수가 하나뿐인 경우, 괄호도 생략할 수 있습니다.
화살표(->) : 매개 변수와 몸체를 구분짓는 역할을 합니다.
몸체 : 람다의 실행 로직을 담고 있으며, 식(expression) 또는 문장(statements)이 올 수 있습니다.
식은 단일 실행 결과를 반환하며, 중괄호는 생략할 수 있습니다.
문장은 중괄호 안에 작성되며, 여러 줄의 코드를 포함할 수 있습니다.
1.4 예시
Thread 실행하기
new Thread(() -> System.out.println("Hello from a thread")).start();
리스트의 각 요소 출력하기
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> System.out.println(item));
Comparator를 통한 정렬
List<String> cities = Arrays.asList("Seoul", "New York", "London");
Collections.sort(cities, (s1, s2) -> s1.compareTo(s2));
1.5 📝 정리.
람다 표현식은 이벤트 리스너, 스레드의 실행 코드 등 여러 곳에서 기존에 익명 클래스를 사용하던 부분을 대체하여 코드를 더 간결하게 만들 수 있습니다.
또한, 스트림 API와 함께 사용될 때 강력한 데이터 처리 기능을 제공하여 복잡한 컬렉션 처리를 단순화시킬 수 있습니다.
2. 람다식의 장점.
자바에서 람다식(Lambda Expression)을 사용하는 것은 여러 가지 장점을 제공합니다.
이러한 장점들은 프로그래밍 스타일, 코드의 간결성, 효율성 및 기능성 측면에서 특히 두드러집니다.
2.1 람다식의 주요 장점들.
1. 코드의 간결성 : 람다식을 사용하면 복잡한 익명 클래스를 사용할 필요가 없어지므로 코드를 훨씬 간결하게 작성할 수 있습니다.
예를 들어, 스레드를 실행하거나 이벤트 리스너를 설정할 때 몇 줄의 코드로 작성할 수 있습니다.
2. 가독성 향상 : 람다식은 기존의 익명 클래스보다 훨씬 읽기 쉽고 이해하기 쉬운 코드를 작성할 수 있게 합니다.
이는 유지보수 시간을 줄이고 코드의 질을 향상시키는 데 도움이 됩니다.
3. 함수형 프로그래밍 지원 : 람다식은 자바에 함수형 프로그래밍 스타일을 도입하였습니다.
이는 데이터 처리와 조작을 보다 선언적으로 표현할 수 있게 해, 복잡한 데이터 처리 로직을 간결하고 효율적으로 작성할 수 있도록 합니다.
4. 코드의 재사용성 증가 : 람다식을 사용하면 특정 동작을 수행하는 코드 블록을 쉽게 재사용할 수 있습니다.
람다식은 변수처럼 취급될 수 있어, 메소드에 인자로 전달하거나 변수에 할당하여 다른 위치에서 사용할 수 있습니다.
5. 병렬 실행 용이 : 자바 8 이후로 스트림 API와 결합된 람다식은 컬렉션 처리를 병렬로 쉽게 수행할 수 있게 해줍니다.
이는 ‘parallelStream()’ 과 같은 메소드를 사용하여 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 합니다.
6. 지연 실행(Lazy Evaluation) : 람다식은 지연 실행을 가능하게 합니다.
예를 들어, 조건이 충족될 때까지 코드 실행을 지연시키거나, 필요할 때만 데이터를 처리하도록 할 수 있습니다.
이는 성능 최적화에 도움을 줄 수 있습니다.
7. 함수 인터페이스와의 호환성 : 람다식은 단일 추상 메소드를 가진 인터페이스(함수 인터페이스)와 호환됩니다.
이는 많은 내장 함수 인터페이스(‘Runnable’, ‘Callable’, ‘Comparator’ 등)와 사용자 정의 함수 인터페이스에 람다식을 적용할 수 있음을 의미합니다.
2.2 📝 정리.
이러한 장점들로 인해 람다식은 자바 프로그래머들 사이에서 매우 인기 있는 기능이 되었으며, 모던 자바 코드에서는 필수적인 요소로 자리 잡고 있습니다.
3. 람다식의 단점.
자바에서 람다식을 사용하면 여러 가지 장점이 있지만, 몇 가지 단점 또는 주의할 점도 있습니다.
3.1 람다식의 사용과 관련된 단점.
1. 디버깅의 어려움 : 람다식은 익명 함수이기 때문에 디버깅이 더 복잡할 수 있습니다.
스택 트레이스에서 람다식이 어디에 위치하는지 명확하게 표시되지 않아 오류를 추적하기 어려울 수 있습니다.
2. 코드의 남용 : 람다식을 과도하게 사용하면 코드가 오히려 더 복잡해지고 이해하기 어려워질 수 있습니다.
특히 람다 내부에 긴 로직이나 조건문을 넣을 경우 가독성이 떨어질 수 있습니다.
3. 람다 캡처링 오버헤드 : 람다식은 주변 환경의 변수를 캡처(Capture)할 수 있습니다.
이 때, 람다가 외부 변수를 캡처 할 경우 추가적인 비용이 발생할 수 있으며, 이는 성능에 영향을 줄 수 있습니다.
4. 직렬화의 문제 : 람다식은 기본적으로 직렬화가 보장되지 않습니다.
따라서 람다식을 사용하는 객체를 직렬화하려고 할 때 문제가 발생할 수 있습니다.
이는 분산 시스템에서 특히 중요한 이슈가 될 수 있습니다.
5. 학습 곡선 : 자바 8 이전의 버전에 익숙한 개발자들에게 람다식과 스트림 API는 새로운 패러다임을 익혀야 하는 도전과제를 제시합니다.
이는 초기 학습 곡선을 가파르게 만들 수 있습니다.
6. 타입 추론의 복잡성 : 람다식에서는 자바 컴파일러가 타입을 추론해야 하는데, 때때로 이 추론이 개발자의 의도와 다른게 이루어질 수 있습니다.
이는 코드의 명확성을 떨어뜨릴 수 있습니다.
7. 함수형 인터페이스의 제약 : 람다식은 단 하나의 추상 메소드를 가진 함수형 인터페이스와 함꼐 사용됩니다.
때로는 이런 제약이 프로그램 설계를 더 복잡하게 만들 수 있습니다.
3.2 📝 정리.
람다식의 단점들은 주로 개발과 관련된 트레이드오프와 관련이 있으며, 이러한 단점을 이해하고 적절히 관리한다면 람다식을 효과적으로 사용할 수 있습니다.
-
☕️[Java] HashMap에 key 값은 항상 int 여야 할까요?
🤔 HashMap에 key 값은 항상 int 여야 할까요?
강의와 예제 코드를 열심히 보고 따라서 타이핑하고 있던 중 “문뜩!” 떠올랐습니다. 🤩
‘HashMap에 key 값은 항상 int 여야 할까요?🤔’
그래서 구글링과 챗 지피티 그리고 Java의 정석 도서를 살펴본 후 이 글을 쓰게 되었습니다 :)
🙅♂️ 대답은 “아니오!” 입니다.
자바 프로그래밍에서 ‘HashMap’ 의 키 값은 ‘int’ 형일 필요는 없다고 합니다.
‘HashMap’ 은 키로서 어떠한 객체도 사용할 수 있으며, 기는 자바의 ‘제네릭’ 을 통해 다양한 유형의 객체를 키로 사용할 수 있게 해준다고 합니다.
(오! “제네릭” 은 아직 안배웠지만 🥲 Swift에서 봐서 비슷한 느낌 같은데?!)
키 객체는 ‘Object’ 클래스의 ‘hashCode()’ 메소드와 ‘equals()’ 메소드를 적절히 구현해야 합니다.
(‘Object’ 클래스는 무엇이고, ‘hashCode()’ 메소드와 ‘equals()’ 메소드는 무엇인가?!! 🤪)
이는 ‘HashMap’ 이 키의 해시 코드를 사용하여 데이터를 저장하고 검색하기 때문입니다.
(도통 무슨 소리인지 몰라서 아래 “제네릭”. “Object 클래스”, “hashCode()”, “equals()”를 정리했어요 ㅎㅎ)
‘HashMap’ 을 사용할 때, 키로 사용되는 객체의 ‘hashCode()’ 메소드가 효율적이고 일관성 있는 값을 반환해야 합니다.
또한, ‘equalse()’ 메소드는 객체의 동등성을 정확하게 판단할 수 있어야 합니다.
이 두 메소드의 구현이 적절히 이루어져야 ‘HashMap’ 이 키의 중복 없이 정확하게 데이터를 관리할 수 있습니다.
예시 - String 객체를 키로 사용하는 ‘HashMap’
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
System.out.println(map.get("two")); // 출력: 2
}
}
위 예시에서 보듯, ‘String’ 외에도 사용자가 정의한 어떠한 객체든 ‘hashCode()’ 와 ‘equals()’ 가 적절히 구현되어 있다면 키로 사용할 수 있습니다.
따라서 ‘int’ 만을 키로 사용해야 하는 것은 아닙니다.
1️⃣ 제네릭(Generic).
자바에서 ‘제네릭(Generic)’ 은 클래스, 인터페이스, 메소드를 정의할 때 타입(Type)을 하나의 매개변수처럼 취급하여, 다양한 데이터 타입을 사용할 수 있도록 하는 프로그래밍 기법입니다.
제네릭을 사용하면 컴파일 시점에 타입 안정성을 제공하고, 타입 캐스팅을 줄여 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다.
제네릭 기본 문법.
제네릭은 타입 매개변수를 사용하여 구현됩니다.
타입 매개변수는 보통 한 글자로 표현되며, 일반적으로 다음과 같은 문자를 사용합니다.
‘E’ : Element(컬렉션에서 사용되는 요소)
‘K’ : Key(키)
‘V’ : Value(값)
‘T’ : Type(일반적인 타입)
‘S’, ‘U’, ‘V’ 등 - 두 번째, 세 번째, 네 번째 타입을 나타내기 위해 사용
예시: 제네릭을 사용한 클래스와 메소드
// 제네릭 클래스 예시
public class Box<T> {
private T t; // T는 이 클래스가 다루는 객체의 타입을 매개변수화합니다.
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
// 제네릭 메소드 예시
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.print(element + " ");
}
System.out.println();
}
위 예시에서 ‘Box’ 클래스는 타입 매개변수 ‘T’ 를 사용하여 다양한 타입을 저장하고 반환할 수 있는 범용 컨테이너로 사용됩니다.
‘printArray’ 메소드는 어떤 배열 타입도 받아들일 수 있으며, 그 요소들을 출력합니다.
2️⃣ Object 클래스.
자바 프로그래밍에서 ‘Object’ 클래스는 자바의 클래스 계층 구조에서 가장 상위에 위치하는 클래스입니다.
모든 자바 클래스는 직접적이거나 간접적으로 ‘Object’ 클래스를 상속받습니다.
이는 ‘Object’ 클래스가 자바에서 모든 클래스의 근본(base)이라는 의미 입니다.
‘Object’ 클래스는 자바의 ‘java.lang’ 패키지에 포함되어 있으며, 모든 객체에 공통적으로 필요한 메서드를 제공합니다.
Object 클래스의 의의.
‘Object’ 클래스의 메서드들은 자바의 모든 클래스에 기본적인 기능을 제공합니다.
이로 인해, 개발자는 어떤 클래스를 만들 때도 이러한 기본적인 메서드들을 새로 작성하지 않고도, 필요에 따라 이를 상속받아 확장하거나 재정의할 수 있습니다.
‘Object’ 클래스는 자바의 모든 클래스와 객체에 공통적인 근복적인 메커니즘을 제공하는 중추적인 역할을 합니다.
3️⃣ Object 클래스의 hashCode() 메소드.
자바의 ‘Object’ 클래스에서 ‘hashCode()’ 메소드는 객체의 메모리 주소를 기반으로 계산된 정수 값을 반환하는 메소드입니다.
이 메소드는 객체의 해시 코드를 제공하며, 해시 기반 컬렉션(예: ‘HashMap’, ‘HashSet’, ‘Hashtable’ 등)에서 객체를 효율적으로 관리하기 위해 사용됩니다.
hashCode() 메소드의 주요 용도
1. 해시 테이블 사용 : ‘hashCode()’ 는 특히 해시 테이블을 사용하는 자료 구조에서 중요합니다.
객체의 해시 코드를 사용하여, 해당 객체가 저장되거나 검색될 해시 버킷을 결정합니다.
이로 인해 데이터의 삽입, 검색, 삭제 작업이 빠르게 수행될 수 있습니다.
2. 객체의 동등성의 빠른 검증 : ‘hashCode()’ 메소드는 ‘equals()’ 메소드와 함께 사용되어 객체의 동등성을 검사합니다.
두 객체가 같다면 반드시 같은 해시 코드를 반환해야 합니다.
따라서, 해시 코드가 다른 두 객체는 결코 같을 수 없으므로, ‘equals()’ 호출 전에 해시 코드를 먼저 확인함으로써 불필요한 비교를 줄일 수 있습니다.
4️⃣ Object 클래스의 equals() 메소드.
자바 프로그래밍에서 ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체가 동등한지 비교하는데 사용됩니다.
이 메소드는 ‘Object’ 클래스에서 모든 클래스로 상속되며, 특히 객체의 동등성을 판단할 때 중요한 역할을 합니다.
기본적으로, ‘Object’ 클래스의 ‘equals()’ 메소드는 두 객체의 참조가 같은지 확인합니다.
즉, 두 객체가 메모리상에서 같은 위치를 가리키는지 검사합니다.
-
☕️[Java] 컬렉션 프레임워크
1️⃣ 컬렉션 프레임워크
1. 컬렉션 프레임워크(Collection Framework)
자바 컬렉션 프레임워크는 자료 구조를 효율적으로 관리하고 조작할 수 있는 방법을 제공하는 통합 아키텍처입니다.
이 프레임워크는 다양한 인터페이스와 구현을 포함하며, 다양한 종류의 컬렉션들을 제어하고, 데이터 집합을 효율적으로 관리하기 위한 알고리즘을 제공합니다.
1.2 컬렉션 프레임워크의 구요 구성 요소.
1. 인터페이스(Interface) : 컬렉션 프레임워크의 핵심 인터페이스로는 ‘Collection’, ‘List’, ‘Queue’ 등이 있으며 각각 다른 형태의 데이터 집합을 추상화합니다.
예를 들어, ‘List’ 는 순서가 있는 데이터 집합을, ‘Set’ 은 중복을 허용하지 않는 데이터 집합을 나타냅니다.
2. 구현(Implementation) : 이러한 인터페이스를 실제로 구현한 클래스들로, ‘ArrayList’, ‘LinkedList’, ‘HashSet’, ‘TreeSet’, ‘PriorityQueue’ 등이 포함됩니다.
각 클래스는 컬렉션 인터페이스를 구현하며, 데이터의 특성에 따라 선택하여 사용할 수 있습니다.
3. 알고리즘(Algorithm) : 컬렉션 데이터를 처리하는 데 필요한 다양한 알고리즘이 제공됩니다.
이 알고리즘은 정렬, 검색, 순환 및 변환 등을 포함하며, 이들은 대부분 ‘Collections’ 클래스에 정적 메소드로 제공됩니다.
1.3 📝 정리.
컬렉션 프레임워크를 사용하면 데이터를 보다 효율적으로 처리할 수 있고, 기능의 재사용성 및 유지 보수성이 향상됩니다.
또한, 자바 개발자로서 다양한 데이터 컬렉션을 쉽게 처리하고, 표준화된 방법으로 데이터를 조작할 수 있는 능력을 갖추게 됩니다.
2. List 인터페이스.
자바 프로그래밍에서 ‘List’ 인터페이스는 ‘java.util’ 패키지의 일부로, 순서가 있는 컬렉션을 나타냅니다.
이 인터페이스를 사용하면 사용자가 목록의 특정 위치에 접근, 삽입, 삭제를 할 수 있는 동시에, 목록의 요소들이 입력된 순서대로 저장 및 관리됩니다.
‘List’ 는 중복된 요소의 저장을 허용하기 때문에, 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
2.1 List 인터페이스의 주요 메서드.
add(E e) : 리스트의 끝에 요소를 추가합니다.
add(int index, E element) : 리스트의 특정 위치에 요소를 삽입합니다.
remove(Object o) : 리스트에서 지정된 요소를 삭제합니다.
remove(int index) : 리스트에서 지정된 위치의 요소를 삭제합니다.
get(int index) : 지정된 위치에 있는 요소를 반환합니다.
set(int index, E element) : 리스트의 특정 위치에 요소를 설정(교체)합니다.
indexOf(Object o) : 객체를 찾고, 리스트 내의 첫 번째 등장 위치를 반환합니다.
size() : 리스트에 있는 요소의 수를 반환합니다.
clear() : 리스트에서 모든 요소를 제거합니다.
2.3 가장 널리 사용되는 구현체.
‘List’ 인터페이스는 다양한 구현체를 가지고 있으며, 가장 널리 사용되는 구현체는 ‘ArrayList’, ‘LinkedList’ 그리고 ‘Vector’ 입니다.
각 구현체는 내부적인 데이터 관리 방식이 다르므로, 사용 상황에 따라 적합한 구현체를 선택할 수 있습니다.
‘ArrayList’ : 내부적으로 배열을 사용하여 요소들을 관리합니다. 인덱스를 통한 빠른 접근이 가능하지만, 크기 조정이 필요할 때 비용이 많이 들 수 있습니다.
‘LinkedList :’ 내부적으로 양방향 연결 리스트를 사용합니다. 데이터의 삽입과 삭제가 빈번하게 일어나는 경우 유용합니다.
Vector : ‘ArrayList’ 와 비슷하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
2.4 📝 정리.
이러한 특성들로 인해 ‘List’ 인터페이스는 자바에서 데이터를 순차적으로 처리할 필요가 있는 다양한 애플리케이션에서 중요하게 사용됩니다.
3. Set 인터페이스.
자바 프로그래밍에서 ‘Set’ 인터페이스는 ‘java.util’ 패키지의 일부이며, 중복을 허용하지 않는 요소의 컬렉션을 나타냅니다.
‘Set’ 은 ‘Collection’ 인터페이스를 확장하는 인터페이스로서, 집합의 개념을 구현합니다.
이는 각 요소가 컬렉션 내에서 유일하게 존재해야 함을 의미합니다.
인덱스로 요소를 관리하는 ‘List’ 인터페이스와 달리, ‘Set’ 은 요소의 순서를 유지하지 않습니다.
3.1 Set의 주요 특징.
중복 불허 : 같은 요소의 중복을 허용하지 않으며, 이미 ‘Set’ 에 존재하는 요소를 추가하려고 시도하면 그 요소는 컬렉션에 추가되지 않습니다.
순서 보장 없음 : 대부분의 ‘Set’ 구현체는 요소의 저장 순서를 유지하지 않습니다. 그러나 ‘LinkedHashSet’ 과 같은 특정 구현체는 요소의 삽입 순서를 유지할 수 있습니다.
값에 의한 접근 : ‘Set’ 은 인덱스를 사용하지 않고 값에 의해 요소에 접근합니다.
3.2 주요 메서드.
‘Set’ 인터페이스는 ‘Collection’ 인터페이스에서 상속받은 다양한 메소드를 포함합니다.
주요 메서드는 다음과 같습니다.
add(E e): 요소 e를 Set에 추가합니다. 이미 존재하는 요소를 추가하려는 경우, 요소는 추가되지 않고 false를 반환합니다.
remove(Object o): 지정된 객체 o를 Set에서 제거합니다.
contains(Object o): Set이 지정된 객체 o를 포함하고 있는지 여부를 반환합니다.
size(): Set의 요소 개수를 반환합니다.
isEmpty(): Set이 비어 있는지 여부를 반환합니다.
clear(): Set의 모든 요소를 제거합니다
3.3 주요 구현체.
‘Set’ 인터페이스는 여러 가지 방법으로 구현될 수 있으며, 각 구현체는 다른 특성을 가집니다.
HashSet : 가장 널리 사용되는 ‘Set’ 구현체로, 해시 테이블을 사용하여 요소를 저장합니다. 요소의 삽입, 삭제, 검색 작업은 평균적으로 상수 시간(O(1))이 걸립니다.
LinkedHashSet : ‘HashSet’ 의 확장으로, 요소의 삽입 순서를 유지합니다.
TreeSet : 레드-블랙 트리 구조를 사용하여 요소를 저장합니다. 요소는 자연적 순서 또는 비교자에 의해 정렬됩니다.
이로 인해 삽입, 삭제, 검색 작업에 로그 시간(O(log n))이 걸립니다.
3.4 📝 정리.
‘Set’ 인터페이스는 주로 중복을 허용하지 않는 데이터 컬렉션을 다루는 데 사용되며, 특히 요소의 유일성을 보장하는데 유용합니다.
4. Map 인터페이스.
자바에서 ‘Map’ 인터페이스는 ‘java.util’ 패키지에 속하며, 키(key)와 값(value) 쌍으로 이루어진 데이터를 저장하는 자료구조를 정의합니다.
‘Map’ 은 키의 중복을 허용하지 않으면서 각 키는 하나의 값에 매핑됩니다.
값은 중복될 수 있지만, 각 키는 유일해야 합니다.
이러한 특성 때문에 ‘Map’ 은 키를 통해 빠르게 데이터를 검색할 수 있는 효율적인 수단을 제공합니다.
4.1 Map의 주요 특징.
키 기반 데이터 접근 : 키를 사용하여 데이터에 접근하므로, 키에 대한 빠른 검색, 삽입, 삭제가 가능합니다.
키의 유일성 : 같은 키를 다시 ‘Map’ 에 추가하려고 하면 기존 키에 연결된 값이 새 값으로 대체됩니다.
값의 중복 허용 : 같은 값을 가진 여러 키가 ‘Map’ 에 존재할 수 있습니다.
4.2 주요 메서드
‘Map’ 인터페이스는 데이터를 관리하기 위해 다음과 같은 주요 메소드를 제공합니다.
put(K key, V value): 키와 값을 Map에 추가합니다. 이미 키가 존재하면, 해당 키의 값이 새로운 값으로 업데이트 됩니다.
get(Object key): 지정된 키에 연결된 값을 반환합니다. 키가 존재하지 않는 경우, null을 반환합니다.
remove(Object key): 지정된 키와 그 키에 매핑된 값을 Map에서 제거합니다.
containsKey(Object key): Map에 특정 키가 있는지 검사합니다.
containsValue(Object value): Map에 특정 값이 하나 이상 있는지 검사합니다.
keySet(): Map의 모든 키를 Set 형태로 반환합니다.
values(): Map의 모든 값을 컬렉션 형태로 반환합니다.
entrySet(): Map의 모든 “키-값” 쌍을 Set 형태의 Map.Entry 객체로 반환합니다.
size(): Map에 저장된 “키-값” 쌍의 개수를 반환합니다.
clear(): Map의 모든 요소를 제거합니다.
4.3 주요 구현체
‘Map’ 인터페이스의 주요 구현체로는 다음과 같은 클래스들이 있습니다.
HashMap : 가장 일반적으로 사용되는 ‘Map’ 구현체로, 해시 테이블을 사용합니다.
요소의 순서를 보장하지 않으며, 키와 값에 ‘null’ 을 허용합니다.
LinkedHashMap : ‘HashMap’ 을 상속받아 구현된 클래스로, 요소의 삽입 순서를 유지합니다.
이는 순회 시 삽인된 순서대로 요소를 얻을 수 있게 해줍니다.
TreeMap : 레드-블랙 트리를 기반으로 하는 ‘Map’ 구현체로, 모든 키가 자연적 순서대로 정렬됩니다.
정렬된 순서로의 접근이 필요할 때 유용합니다.
Hashtable : ‘HashMap’ 과 유사하지만, 모든 메소드가 동기화되어 있어 멀티스레드 환경에서 사용하기에 안전합니다.
그러나 성능이 ‘HashMap’ 보다 느리고, 키와 값에 ‘null’ 을 허용하지 않습니다.
4.4 📝 정리.
‘Map’ 인터페이스는 다양한 애플리케이션에서 설정, 프로파일, 사용자 세션 등의 데이터를 키와 값의 형태로 관리할 때 유용하게 사용됩니다.
-
-
☕️[Java] 입출력(2)
1️⃣ 입출력(2)
1. 파일 출력.
자바 프로그래밍에서 파일 출력은 프로그램이 데이터를 쓰는 과정을 말합니다.
이 과정을 통해 프로그램은 실행 결과를 저장하거나, 사용자가 입력한 정보를 파일에 기록하고, 다른 프로그램이나 나중에 프로그램 자체가 다시 사용할 수 있는 형태로 데이터를 출력할 수 있습니다.
2. 파일 출력을 수행하기 위한 기본 방법들.
1. FileOutputStream 사용
‘FileOutputStream’ 클래스는 바이트 단위의 출력을 파일에 직접 쓸 때 사용됩니다.
이 클래스를 사용하면 이미지, 비디오 파일, 이진 데이터 등을 파일로 저장할 수 있습니다.
```java
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputExample {
public static void main(String[] args) {
String data = “Hello, this is a test.”;
try (FileOutputStream out = new FileOutputStream(“output.txt”)) {
out.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **2. PrintWriter 사용**
- **'PrintWriter'** 는 문자 데이터를 출력할 때 사용됩니다.
- 이 클래스는 파일에 텍스트를 쓸 때 편리하며, 자동 플러싱 기능, 줄 단위 출력 등의 메소드를 제공합니다.
```java
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriteExample {
public static void main(String[] args) {
try (PrintWriter writer = new PrintWriter(new FileWriter("output.txt", true))) {
writer.println("Hello, this is a test.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. FileWriter 사용
‘FileWriter’ 는 자바에서 파일에 텍스트 데이터를 쓰기 위한 간편한 방법 중 하나입니다.
이 클래스는 내부적으로 문자 데이터를 파일에 쓸 수 있도록 ‘OutputStreamWriter’ 를 사용하여 바이트 스트림을 문자 스트림으로 변환합니다.
‘FileWriter’ 는 텍스트 파일을 쉽게 작성할 수 있도록 해주며, 생성자를 통해 다양한 방식으로 파일을 열 수 있습니다.
```java
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
pulbic static void main(String[] args) {
try (FileWriter writer = new FileWriter(“output.txt”, true)) {
writer.write(“Hello, this is a test.”);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- **4. BufferedWriter 사용**
- **'BufferedWrite'** 는 버퍼링을 통해 효율적으로 파일에 문자 데이터를 쓸 수 있도록 합니다.
- **'FileWriter'** 와 함께 사용되어, 더 큰 데이터를 처리할 때 성능을 개선합니다.
```java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample {
public static void main(String[] args) {
String content = "Hello, this is a test.";
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 파일 입력.
자바 프로그래밍에서 파일 입력은 프로그램이 파일로부터 데이터를 읽어들이는 과정을 말합니다.
이 데이터는 텍스트나 바이너리 형태일 수 있으며, 파일에서 데이터를 읽어 프로그램 내에서 사용할 수 있도록 만드는 것이 목적입니다.
파일 입력을 위해 자바는 다양한 입출력 클래스를 제공합니다.
2.1 주로 사용되는 파일 입력 방법.
1. FileInputStream 사용
‘FileInputStream’ 은 바이트 단위로 파일에서 데이터를 읽는 데 사용됩니다.
이 클래스는 이미지, 비디오 파일, 실행 파일등의 이진 데이터 처리에 주로 사용됩니다.
```java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream(“input.dat”)) {
int content;
while ((content = fis.read()) != -1) {
// content 변수에 한 바이트씩 읽어들인 데이터를 저장
System.out.print((char) content);
}
} catch (IOExecption e) {
e.printStackTrace();
}
}
}
- **2. BufferedRead** 와 **FileReader 사용**
- **'BufferedReader'** 와 **'FileReader'** 는 텍스트 데이터를 효과적으로 읽기 위해 함께 사용됩니다.
- **'FileReader'** 는 파일에서 문자 데이터를 읽어들이며, **'BufferedReader'** 는 버퍼링을 통해 읽기 성능을 향상 시킵니다.
```java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader br new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readline()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. Scanner 사용
‘Scanner’ 클래스는 텍스트 파일을 읽을 때 유용하며, 특히 토큰화(tokenizing)된 데이터를 처리할 때 편리합니다.
‘Scanner’ 는 정규식을 사용하여 입력을 구분자로 분리하고, 다양한 타입으로 데이터를 읽어들일 수 있습니다.
```java
import java.io.File;
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File(“input.txt”))) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
2.2 📝 정리.
이렇게 다양한 방법을 통해 파일로부터 데이터를 읽을 수 있으며, 각 방법은 사용하는 데이터 타입과 처리할 데이터의 양에 따라 선택할 수 있습니다.
파일에서 데이터를 읽는 것은 데이터를 처리하거나, 설정 정보를 불러오거나, 사용자 데이터를 읽는 등 다양한 목적으로 활용됩니다.
-
☕️[Java] 예외 처리
1️⃣ 예외 처리
예외 처리가 무엇인지 이해하고, 예외 처리 방법에 대해 직접 구현
1. 예외(Exception)
자바 프로그래밍에서 “예외(Exception)” 란 프로그램 실행 중에 발생하는 비정상적인 조건 또는 오류를 의미합니다.
이는 프로그램의 정상적인 흐름을 방해하며, 적절히 처리하지 않으면 프로그램이 비정상적으로 종료될 수 있습니다.
자바에서는 이러한 예외를 효과적으로 처리하기 위해 강력한 예외 처리 메커니즘을 제공합니다.
1.2 예외의 유형.
자바에서 예외는 크게 두 가지 유형으로 나눌 수 있습니다.
1. Checked Execptions
컴파일 시간에 체크되는 예외로, 컴파일러가 해당 예외를 처리하도록 요구합니다.
이 예외들은 주로 외부 시스템과의 상호 작요(파일 입출력, 네트워크 통신 등)에서 발생하며, 프로그래머가 이를 적절히 처리하도록 강제합니다.
2. Unchecked Exceptions
런타임에 발생하는 예외로, 주로 프로그래머의 실수로 인해 발생합니다.(예: 배령의 범위를 벗어나는 접근, null 참조 등.)
이러한 예외는 컴파일러가 체크하지 않으므로, 개발자가 예측하고 적절히 처리할 필요가 있습니다.
2. 예외 처리(Exception Handling)
자바 프로그래밍에서 예외 처리는 프로그램 실행 중에 발생할 수 있는 예외적인 상황, 즉 오류나 문제를 안전하게 관리하고 대처하는 방법을 말합니다.
예외 처리를 통해 프로그램의 비정상적인 종료를 막고, 오류 발생 시 적절한 대응을 할 수 있도록 합니다.
이는 프로그램의 안정성과 신뢰성을 높이는 데 중요한 역할을 합니다.
2.1 예외 처리의 주요 구성 요소.
1. try 블록
예외가 발생할 가능성이 있는 코드를 이 블록 안에 넣습니다.
만약 블록 안의 코드 실행 중에 예외가 발생하면, 즉시 해당 블록의 실행을 중단하고 ‘catch’ 블록으로 제어를 넘깁니다.
2. catch 블록
‘try’ 블록에서 발생한 특정 유형의 예외를 처리합니다.
프로그램이 예외를 안전하게 처리할 수 있도록 적절한 로직을 구현할 수 있습니다.
하나의 ‘try’ 블록에 여러 ‘catch’ 블록을 사용하여 다양한 종류의 예외를 각각 다르게 처리할 수 있습니다.
3. finally 블록
이 블록은 예외의 발생 여부롸 관계없이 실행되는 코드를 포함합니다.
주로 사용되는 목적은 자원 해제와 같은 정리 작업을 수행하기 위함입니다.
예를 들어 파일이나 네트워크 자원을 닫거나 해제할 때 사용됩니다.
4. throws 키워드
메소드 선언 시 사용되며, 해당 메소드가 예외를 직접 처리하지 않고 호출한 메소드로 예외를 전파하겠다는 것을 나타냅니다.
이를 통해 예외 처리의 책임을 메소드 호출자에게 넘길 수 있습니다.
2.2 예외 처리 예제.
public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.err.println("Arithmetic Exception: Division by zero is not allowed.");
} finally {
System.out.println("This block is always executed.");
}
}
public static int divide(int numerator, in denominator) {
return numerator / denominator; // This ca throw ArithmeticException if denominator is zero.
}
}
이 예제에서 ‘divide’ 메소드는 분모가 0일 때 ArithmeticException 을 발생시킬 수 있습니다.
‘try’ 블록 안에서 이 메소드를 호출하고, 예외가 발생하면 ‘catch’ 블록에서 이를 잡아서 적절한 오류 메시지를 출력합니다.
또한, ‘finally’ 블록은 예외 발생 여부와 상관없이 실행되어 어떤 상황에서도 실행될 필요가 있는 코드를 포함할 수 있습니다.
3. throw 키워드.
자바 프로그래밍에서 ‘throw’ 키워드는 개발자가 의도적으로 예외를 발생시키기 위해 사용합니다.
이를 통해 특정 상황에서 프로그램의 흐름을 제어하거나, 특정 조건에서 오류를 발생시켜 예외 처리 메커니즘을 테스트하거나 강제할 수 있습니다.
‘throw’ 는 예외 객체를 생성하고 이를 던집니다(throw)
즉, 프로그램의 정상적인 실행 흐름을 중단하고 예외 처리 루틴으로 제어를 이동시킵니다.
3.1 ‘throw’ 사용법.
‘throw’ 를 사용할 때는 예외 객체를 생성해야 합니다.
이 객체는 ‘Throwable’ 클래스 또는 그 하위 클래스의 인스턴스여야 합니다.
자바에서는 대부분 ‘Exception’ 클래스 또는 그 서브클래스를 사용하여 예외를 생성하고 던집니다.
예제.
public class Main {
public static void main(String[] args) {
try {
checkAge(17);
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
}
}
static void checkAge(int age) throws Execption {
if (age < 18) {
throw new Exception("Access denied - You must be at least 18 years old.");
}
System.out.println("Access granted - You are old enough!");
}
}
이 예제에서 ‘checkAge’ 메소드는 나이를 확인하고, 18세 미만인 경우 예외를 던집니다.
이 예외는 ‘throw new Exception(…)’ 을 통해 생성되고 던져집니다.
메인 메소드에서는 이 메소드를 ‘try’ 블록 안에서 호출하고, ‘catch’ 블록을 통해 예외를 잡아서 처리합니다.
결과적으로, 사용자가 18세 미만이면 “Access denided” 메시지를 포함하는 예외가 출력됩니다.
3.2 ‘throw’와 ‘throws’의 차이
‘throw’ : 예외를 실제로 발생시키는 행위입니다. 이는 메소드 내부에서 특정 조건에서 예외를 발생시킬 때 사용됩니다.
‘throws’ : 메소드 선언에 사용되며, 해당 메소드가 실행되는 동안 발생할 수 있는 예외를 명시적으로 선언합니다. 이는 호출자에게 해당 메소드를 사용할 때 적절한 예외 처리가 필요하다는 것을 알립니다.
4. throws 키워드.
자바 프로그래밍에서 ‘throws’ 키워드는 메소드 선언에 사용되며, 해당 메소드가 실행 도중 발생할 수 있는 특정 유형의 예외를 명시적으로 선언하는 데 사용됩니다.
‘throws’ 는 메소드가 예외를 직접 처리하지 않고, 대신 이를 호출한 메소드로 예외를 “던져”(전파하는) 사실을 알립니다.
이를 통해 예외 처리 책임을 메소드 호출자에게 넘기는 것입니다.
4.1 ‘throws’ 사용의 목적.
명시성
메소드가 발생시킬 수 있는 예외를 명시함으로써, 이 메소드를 사용하는 다른 개발자들에게 해당 메소드를 사용할 때 어떤 예외들을 처리해야 하는지 명확하게 알릴 수 있습니다.
강제 예외 처리
‘throws’ 로 선언된 예외는 대부분 “checked exception” 이며, 이는 메소드를 호출하는 코드가 반드시 이 예외들을 처리하도록 강제합니다(try-catch 블록을 사용하거나, 또 다시 ‘throws’ 로 예외를 전파하도록 함).
4.2 ‘throws’ 사용법 예제.
아래 예제에서는 ‘throws’ 를 사용하여 ‘IOException’ 을 처리하는 방법을 보여줍니다.
이 예외는 파일 입출력 작업에서 자주 발생합니다.
import java.io.*;
public class Main {
public static void main(String[] args) {
try {
readFile("example.txt");
} catch (IOExecption e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
public static void readFile(String filename) throws IOException {
File file = new File(filename);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int content;
while ((content = fis.read()) != -1) {
// Process the content
System.out.print((char) content);
}
} finally {
if (fis != null) {
fis.close();
}
}
}
}
이 예제에서 ‘readFile’ 메소드는 파일을 읽을 때 발생할 수 있는 IOException 을 처리하지 않고, 대신 ‘throws’ 키워드를 사용하여 이 예외를 메소드를 호출한 ‘main’ 메소드로 전달합니다.
‘main’ 메소드는 이 예외를 ‘catch’ 블록을 통해 처리합니다.
-
-
☕️[Java] 인터페이스
1️⃣ 인터페이스.
1. 인터페이스(Interface).
자바에서 인터페이스(Interface)는 메서드의 시그니처만을 정의하는 참조 타입입니다.
인터페이스는 클래스가 구현(implement) 해야 하는 동작의 설계를 제공하며, 구현하는 모든 클래스에 대해 특정 메소드들이 반드시 존재하도록 강제합니다.
이는 다형성을 지원하는 강력한 방법으로, 서로 다른 클래스들이 동일한 인터페이스를 구현함으로써 동일한 방식으로 처리될 수 있게 해 줍니다.
1.2 인터페이스의 주요 특징.
1. 메소드 선언만 포함 : 인터페이스는 메소드의 구현을 포함하지 않습니다.(자바 8 이전까지는).
메소드의 몸체는 인터페이스를 구현하는 클래스에서 제공됩니다.
2. 상수만 포함 가능 : 인터페이스는 상수만을 멤버로 가질수 있습니다.
모든 필드는 ‘public’, ‘static’, ‘final’ 로 선언됩니다.
3. 다중 구현 지원 : 한 클래스는 여러 인터페이스를 구현할 수 있으며, 이를 통해 다중 상속의 이점을 얻을 수 있습니다.
4. 디폴트 메소드와 정적 메소드 : 자바 8 이후부터는 인터페이스에 디폴트 메소드(구현을 포함하는 메소드)와 정적 메소드를 정의할 수 있게 되었습니다.
이를 통해 더 유연한 설계가 가능해졌습니다.
1.3 인터페이스 정의 예시.
public interface Vehicle {
void start();
void stop();
}
위 예제에서 ‘Vehicle’ 인터페이스는 ‘start’ 와 ‘stop’ 이라는 두 메소드를 정의합니다.
이 인터페이스를 구현하는 모든 클래스는 이 두 메소드의 구체적인 구현을 제공해야 합니다.
1.4 인터페이스 구현 예.
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starts.");
}
@Override
public void stop() {
System.out.println("Car stops.");
}
}
‘Car’ 클래스는 ‘Vehicle’ 인터페이스를 구현합니다.
이 클래스는 start 와 ‘stop’ 메소드를 구체적으로 구현해야 합니다.
1.5 결론.
인터페이스는 클래스와 다른 클래스 사이의 계약을 정의하고, 특정 작업을 수행하는 메소드의 시그니처를 강제합니다.
이는 코드의 상호 운용성을 높이고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
인터페이스를 사용함으로써 다양한 구현체를 동일한 방식으로 처리할 수 있어, 코드의 유지보수성과 확장성이 향상됩니다.
2. 상수(constant).
자바 프로그래밍에서 상수(constant)는 값이 선언 후 변경될 수 없는 변수를 의미합니다.
상수는 일반적으로 프로그램 전체에서 변하지 않는 값에 사용되며, 이는 코드의 읽기 쉬움과 유지 관리를 돕습니다.
자바에서 상수를 선언하기 위해 ‘final’ 키워드를 변수 선언과 함께 사용합니다.
2.1 상수의 특징.
1. 불변성 : 상수는 한 번 초기화되면 그 값이 변경될 수 없습니다.
2. 명확성 : 코드 내에서 직접적인 값보다는 의미 있는 이름을 가진 상수를 사용함으로써 코드의 가독성과 유지보수성이 향상됩니다.
3. 공용 사용 : 자주 사용되는 값이나 의미가 명확한 수치를 상수로 선언하여 코드 전바에 걸쳐 재사용할 수 있습니다.
2.2 상수 선언 예시.
상수를 선언하는 방법은 간단합니다.
‘final’ 키워드를 사용하여 변수를 선언하고, 초기화합니다.
일반적으로 상수의 이름은 대문자로 표기하며, 단어 간에는 언더스코어(‘_‘)를 사용합니다.
이는 상수임을 쉽게 식별할 수 있도록 도와줍니다.
public class Constants {
public static final int MAX_WIDTH = 800;
public static final int MAX_HEIGHT = 600;
public static final String COMPANY_NAME = "MyCompany";
}
위 예에서 ‘MAX_WIDTH’, ‘MAX_HEIGHT’, ‘COMPANY_NAME’ 은 모두 상수이며, 이들의 값은 선언된 후 변경될 수 없습니다.
2.3 상수 사용의 이점.
오류 감소 : 값이 한 번 설정되면 변경되지 않기 때문에, 예상치 못한 곳에서 값이 변경되어 발생할 수 있는 버그를 줄일 수 있습니다.
코드 재사용성 : 한 곳에서 값을 변경하면, 해당 상수를 사용하는 모든 위치에서 변경된 값이 적용됩니다. 이는 일관성 유지와 함께 코드 관리를 간소화합니다.
컴파일 시간 최적화 : 상수 값은 컴파일 시간에 결정되므로, 런타임에 추가적인 계산 비용이 들지 않습니다.
2.4 결론.
상수는 프로그램 내에서 변하지 않는 값을 나타내며, 코드의 안정성과 유지보수성을 높이는 데 중요한 역할을 합니다.
자바에서는 ‘final’ 키워드를 사용하여 이러한 상수를 쉽게 생성할 수 있습니다.
3. 클래스의 상속과 인터페이스의 구현을 동시에 사용.
자바에서는 클래스의 상속과 인터페이스의 구현을 동시에 사용하여 “다중 상속“과 유사한 효과를 얻을 수 있습니다.
이는 자바의 설계에서 클래스는 단일 상속만을 허용하지만, 인터페이스는 다중으로 구현할 수 있게 함으로써 이루어집니다.
3.1 단일 상속과 다중 인터페이스 구현.
단일 상속 : 자바에서 클래스는 단 하나의 상위 클래스만 상속받을 수 있습니다.
이는 C++ 같은 언어에서 볼 수 있는 다중 상속의 복잡성과 관련된 문제(예: 다이아몬드 문제)를 피하기 위함입니다.
다중 인터페이스 구현 : 한 클래스는 여러 인터페이스를 구현할 수 있습니다.
이는 인터페이스가 구체적인 구현을 포함하지 않기 때문에(자바 8 이전까지, 자바 8 이후에는 디폴트 메소드를 통해 일부 구현을 포함할 수 있음), 클래스가 여러 인터페이스를 구현함으로써 다중 상속의 효과를 나타낼 수 있습니다.
3.2 예시.
다음 예시에서 ‘Car’ 클래스는 ‘Vehicle’ 클래스를 상속받고, ‘Electric’ 및 ‘Autonomous’ 두 인터페이스를 구현하고 있습니다.
이를 통해 ‘Car’ 클래스는 ‘Vehicle’ 클래스의 속성과 메소드를 상속받으며, 동시에 두 인터페이스의 메소드를 구현해야 합니다.
class Vehicle {
void drive() {
System.out.println("This vehicle is driving.");
}
}
interface Electric {
void charge();
}
interface Autonomous {
void navigate();
}
class Car extends Vehicle implements Electric, Autonomous {
@Override
public void charge() {
System.out.println("The car is charging.");
}
@Override
public void navigate() {
System.out.println("The car is navigating autonomously.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.drive();
myCar.charge();
myCar.navigate();
}
}
3.4 결론.
자바에서는 한 클래스가 단일 상속을 통해 한 클래스의 기능을 상속받고, 동시에 여러 인터페이스를 구현함으로써 다중 상속의 효과를 얻을 수 있습니다.
이는 자바의 타입 시스템이 제공하는 유연성을 활용하는 좋은 예시로, 소프트웨어 설계에서 필요한 다양한 기능을 조합할 수 있게 해 줍니다.
-
-
☕️[Java] 내부 클래스
1️⃣ 내부 클래스.
내부 클래스의 개념과 종류 이해
익명 클래스 직접 구현
1. 내부 클래스(Inner Class).
자바 프로그래밍에서 내부 클래스(Inner Class)는 다른 클래스의 내부에 정의된 클래스를 말합니다.
내부 클래스는 주로 외부 클래스와 밀접한 관련이 있으며, 외부 클래스의 멤버들에 대한 접근을 용이하게 하기 위해 사용됩니다.
1.1 내부 클래스의 특징.
자바의 내부 클래스에는 몇 가지 특징이 있습니다.
이 특징들은 내부 클래스가 어떻게 사용되고, 그들이 주는 이점과 한계를 이해하는 데 도움이 됩니다.
1. 접근성과 밀접성 : 내부 클래스는 외부 클래스의 모든 필드와 메소드(프라이빗 포함)에 접근할 수 있습니다. 이는 내부 클래스가 외부 클래스와 밀접한 작업을 수행할 때 매우 유용합니다.
이러한 접근은 내부 클래스가 외부 클래스의 구현 세부사항에 깊이 연결될 수 있게 합니다.
2. 캠슐화 증가 : 내부 클래스를 사용하면 관련 있는 부분만을 그룹화하여 외부에 불필요한 정보를 노출하지 않고도 복잡한 코드를 더 잘 구조화할 수 있습니다.
이는 코드의 유지보수성과 가독성을 높이는 데 도움이 됩니다.
3. 코드의 응집성 : 내부 클래스는 특정 외부 클래스와 매우 강하게 연결되어 있기 때문에, 그 기능이 외부 클래스와 밀접하게 관련된 기능을 수행할 때 코드의 응집력을 높일 수 있습니다.
4. 더 나은 논리적 그룹핑 : 특정 기능을 내부 클래스에 구현함으로써, 관련 기능과 데이터를 함께 논리적으로 그룹화할 수 있습니다.
이는 전체 코드베이스를 통해 일관성을 유지하고, 기능별로 코드를 정리하는 데 도움이 됩니다.
5. 명시적인 컨텍스트 연결 : 내부 클래스는 명시적으로 그들의 외부 클래스의 인스턴스와 연결됩니다. 이는 그들이 외부 클래스의 상태와 행동에 따라 다르게 작동할 수 있음을 의미합니다.
6. 다중 상속의 일종의 구현 : 자바는 다중 상속을 지원하지 않지만, 내부 클래스를 통해 비슷한 효과를 낼 수 있습니다. 외부 클래스가 하나 이상의 내부 클래스를 가질 수 있고, 각 내부 클래스는 다른 클래스를 상속받을 수 있으므로 다양한 기능을 조합할 수 있습니다.
7. 메모리 및 성능 고려사항 : 내부 클래스는 외부 클래스의 인스턴스와 연결되어 있기 때문에, 외부 클래스의 인스턴스가 메모리에 남아 있는 동안에는 가비지 컬렉션에서 제거되지 않습니다. 이는 메모리 관리 측면에서 고려해야 할 사항입니다.
1.2 내부 클래스의 네 가지 유형.
1. 비정적 중첩 클래스(Non-static Nested Class) 또는 내부 클래스(Inner Class) : 이 클래스는 외부 클래스의 인스턴스 멤버처럼 동작하며, 외부 클래스의 인스턴스에 대한 참조를 가지고 있습니다. 외부 클래스의 인스턴스 멤버와 메소드에 접근할 수 있습니다.
2. 정적 중첩 클래스(Static Nested Class) : 이 클래스는 외부 클래스의 정적 멤버처럼 동작하며, 외부 클래스의 인스턴스 멤버에는 접근할 수 없지만, 정적 멤버에는 접근할 수 있습니다.
3. 지역 클래스(Local Class) : 특정 메소드 또는 초기화 블록 내부에 정의된 클래스로, 선언된 영역 내에서만 사용할 수 있습니다. 지역 클래스는 해당 메소드 내에서만 사용되므로, 외부로 노출되지 않습니다.
4. 익명 클래스(Anonymous Class) : 이름이 없는 클래스로, 일반적으로 단 한 번만 사용되며 주로 리스너(listener) 또는 작은 델리게이션 클래스로 사용됩니다. 클래스 선언과 인스턴스 생성이 동시에 이루어집니다.
-
-
📝[blog post] 프론트엔드와 백엔드는 무엇이 다를까?(+내가 백엔드 개발자가 되고 싶은 이유)
1️⃣ 프론트엔드와 백엔드?
처음 이 글의 여정을 함께하기에 앞서 프론트엔트가 무엇인가 백엔드가 무엇인지 알아야 할 것 같아요!
제가 아무것도 모르는 당시 저 두 단어 “프론트엔드”, “백엔드”를 듣고 느낀 것은
“프론트엔드”는 뭔가 프론트 데스크 같이 앞에서 누군가가 나를 반겨주는 느낌이였고, “백엔드”는 뒤쪽에서 나를 받쳐주는 든든한 느낌이랄까? 😆
그저 느낌으로는 알쏭달쏭하니 정확한 의미를 알아보는 여행을 떠나봅시다! 🙋♂️
2️⃣ 프론트엔드.
프론트엔드는 웹사이트에서 우리가 볼 수 있는 모든 것들을 만드는 일을 말해요 😆
예를 들어, 컴퓨터나 핸드폰으로 책을 보거나 게임을 할 때, 그 화면에 보이는 모든 것들이 바로 프론트엔드에서 만들어진 거예요.(존경합니다 프론트엔드 개발자님들🙇♂️)
이렇게 생각해 볼까요?
웹사이트를 마치 컬러링북처럼 생각한다면, 프론트엔드 개발자는 그림을 그리고 색칠하는 사람이에요 🧑🎨
프론트엔드 개발자들은 화면에 나타날 모양이나 색상을 정하고, 어디를 누르면 어떤일이 일어날지도 결정합니다.
예를 들어, ‘스타드’ 버튼을 누르면 게임이 시작되거나, 사진을 클릭하면 커지는 것처럼 말이에요.
즉, 프론트엔드는 우리가 웹사이트에서 보고 만지는 모든 것을 아름답고 재미있게 만들어 주는 중요한 일을 한답니다!
3️⃣ 백엔드.
백엔드는 웹사이트에서 우리가 눈에 보이지 않는 부분을 다루는 일을 해요.(그렇다고 뭐.. 해커 이런건 아닙니다.. 완전히 달라요…)
이것은 마치 마술사가 무대 뒤에서 마술을 준비하는 것과 비슷해요! 🪄
우리가 볼 수는 없지만, 마술이 멋기제 보이도록 도와주죠.
예를 들어, 우리가 컴퓨터로 쇼핑을 할 때, 옷이나 장난감을 고르고 주문 버튼을 눌러요. 이떄 백엔드는 주문한 것이 무엇인지 기억하고, 그 물건을 어디로 보내야 할지 알려줘요.
또한, 우리가 어떤 게임을 하거나 질문을 할 때도, 백엔드는 그 대답을 찾아서 화면에 보여주죠.
백엔드는 컴퓨터와 데이터베이스라는 큰 저장소를 사용해서, 우리가 웹사이트에서 필요한 모든 정보를 처리하고 저장하는 곳이에요.
우리가 보지 못하지만, 웹사이트가 잘 작동하도록 도와주는 매우 중요한 부분이랍니다!
4️⃣ 내가 백엔드 개발자가 되고 싶은 이유.
저는 어렸을 때 레고를 참 좋아했어요 :)
그 중에서도 테크닉 레고를 가장 좋아했었어요 :)
그 이유는 완성된 것을 보는 것도 좋았지만 조립해 나가면서 그 안에 중심이 되는 코어, 즉 움직임의 중앙부를 제가 직접 조립하고 움직임이 어디서부터 시작되는지를 직접 이해하는 것이 너무 재미있었거든요.
자동차 레고를 만들다보면 직접 엔진를 만들게 됩니다.
그러면 진짜 엔진이 어떻게 움직이고 이 엔진이 어떻게 동작하느냐에 따라 자동차의 다른 부품들이 맞물려 하나씩 동작하는지 상상되는게 너무 행복했었어요.
이런것들이 어렸을 때부터 너무 좋았답니다.
그리고나서 조금 커서는 루어 낚시를 좋아하게 되었어요.
이 루어 낚시는 “배스” 라는 어종을 대상으로 하는 낚시인데, 이 어종에 대한 여러가지 공부를 해야 했었어요.
먼저, 이 어종이 온도에 민감해 온도에 따라 공격 패턴이 달라요 그래서 그 패턴에 대한 데이터를 수집해야 했었어요.
두 번째, 이 어종은 수중 구조물에 굉장히 예민해요. 자신이 좋아하는 수중 구조물이 따로 있어서 그 수중 구조물을 따로 탐색하고 이해하는 법을 배워야 했었어요.
세 번째, 날씨에 영향을 많이 받는 어종이에요. 햇빛과 그늘 그리고 비가 오는 날과 안오는 날에 따라 먹이 사냥 패턴이 달라져요. 그에 따른 루어 선택과 패턴을 다르게 골라야 합니다.
네 번째, 피딩 타임이라는 이 어종의 먹이 사냥 시간이 있습니다. 이 시간에 따라 어종의 먹이 사냥 패턴이 매우 다양해요.
마지막, 계절에 따라 이 어종이 물 속이 바닥, 중층 또는 상층에 머무는지 이런 데이터가 달라요.
이렇게 이 어종을 낚기 위해서는 수 많은 변수와 데이터들을 조합하여 적절한 위치에 적합한 루어를 선택하여 공격 패턴에 맞는 액션을 주어야 배스가 물어 줍니다.
그럴때 “아 나의 데이터가 맞았구나!” 하는 희열감과 아드레날린 그리고 도파민이 폭발해버리죠.
이런 특성이 저는 백엔드에서도 비슷하게 적용되는 것 같아요.
레고는 백엔드에서의 중심 동작을 알아가는 과정과 직접 동작하는 로직을 만드는 부분에서의 즐거움을 찾아가는 과정에서 재미를 느끼고,
낚시는 백엔드에서 데이터를 찾고 뽑아내어 가공하고 내어주는 부분에서 희열을 느끼는 것 같습니다.
그래서 저의 적성과 맞는 것 같아요.
저는 이러한 부분에서 백엔드 개발자가 제가 즐길 수 있는 부분이 서로 맞기 때문에 백엔드 개발자가 되고 싶습니다 😆
-
☕️[Java] 다형성
1️⃣ 다형성.
1. 다형성(Polymorphism)
자바에서 말하는 다형성(Polymorphism)은 객체가 여러 형태를 취할 수 있는 능력을 말합니다.
이는 같은 이름의 메소드 호출이 객체의 타입에 따라 다은 동작을 수행할 수 있게 해 주어 코드의 유연성과 재사용성을 증가시킵니다.
자바에서는 주로 두 가지 형태의 다형성을 지원하는데, 이는 컴파일 시간 다형성과 런타임 다형성입니다.
1.2. 컴파일 시간 다형성(정적 다형성).
컴파일 시간 다형성은 주로 메소드 오버로딩을 통해 구현됩니다.
메소드 오버로딩은 동일한 메소드 이름을 가지면서 매개변수 타입, 순서, 개수가 다른 여러 메소드를 같은 클래스 내에 선언하는 것을 의미합니다.
이러한 메소드들은 컴파일 시에 그 타입에 따라 구별되어 처리됩니다.
1.3 컴파일 시간 다형성 예시.
public class Display {
public void print(int num) {
System.out.println("Printing integer: " + num);
}
public void print(String str) {
System.out.println("Printing string: " + str);
}
}
1.4 런타임 다형성(동적 다형성).
런타임 다형성은 메소드 오버라이딩을 통해 구현됩니다.
이 경우 서브클래스에서 상속받은 부모 클래스의 메소드를 재정의하여 동일한 메소드 호출이 서로 다른 클래스 객체에 대해 다른 동작을 할 수 있도록 합니다.
이는 실행 중에 결정되므로 동적 다형성이라고 합니다.
1.5 런타임 다형성 예시.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Dog();
myAnimal.sound(); // 출력: Dog barks
myAnimal = new Cat();
myAnimal.sound(); // 출력: Cat meows
}
}
여기서 ‘Animal’ 클래스의 ‘sound()’ 메소드는 ‘Dog’ 와 Cat 클래스에서 오버라이딩되었습니다.
‘myAnimal’ 참조 변수는 ‘Animal’ 타입이지만, 참조하는 객체의 실제 타입에 따라 적절한 ‘sound()’ 메소드가 호출됩니다.
1.6 다형성의 장점.
유연성 : 다형성을 사용하면 프로그램을 더 유연하게 설계할 수 있습니다.
예를 들어, 다양한 지식 클래스의 객체들을 부모 클래스 타입의 컬렉션에 저장하고, 각 객체에 대해 공통된 인터페이스를 통해 작업을 수행할 수 있습니다.
코드 재사용과 유지 보수의 향상 : 공통 인터페이스를 사용함으로써 코드를 재사용하고, 새로운 클래스 타입을 추가하거나 기존 클래스를 수정할 때 유지 보수가 용이해집니다.
📝 정리.
이렇게 다형성은 객체 지향 프로그래밍의 중요한 특성 중 하나로, 프로그램의 다양한 부분에서 유용하게 활용됩니다.
2. instanceof
자바 프로그래밍에서 ‘instanceof’ 연산자는 특정 객체가 지정한 타입의 인스턴스인지를 검사하는 데 사용됩니다.
이 연산자는 객체의 타입을 확인할 때 유용하게 쓰이며, 주로 객체의 실제 타입을 판별하여 안전하게 형 변환을 하기 전이나 특정 타입에 따른 조건 분기를 실행할 때 사용됩니다.
2.1 instanceof 연산자의 사용법.
‘instanceof’ 는 구 개의 피 연산자를 비교합니다.
왼쪽 피연산자는 객체를 나타내며, 오른쪽 피연산자는 타입(클래스나 인터페이스)을 나타냅니다.
연산의 결과는 불리언 값입니다.
만약 왼쪽 피연산자가 오른쪽 피연산자가 지정하는 타입의 인스턴스면 ‘true’ 를, 그렇지 않으면 ‘false’ 를 반환합니다.
기본 구조
if (object instanceof ClassName) {
// 조건이 참일 때 실행될 코드
}
예시
class Animal {}
class Dog extends Animal {}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Animal animalDog = new Dog();
System.out.println(animal instanceof Animal); // true
System.out.println(dog instanceof Animal); // true
System.out.println(animalDog instanceof Animal); // true
System.out.println(animal instanceof Dog); // false
}
}
이 예시에서 ‘dog instanceof Animal’ 은 ‘true’ 를 반환합니다.
왜냐하면 ‘Dog’ 클래스가 ‘Animal’ 클래스의 서브클래스이기 때문입니다.
하지만 ‘animal instanceof Dog’ 은 ‘false’ 를 반환하는데, 이는 ‘Animal’ 인스턴스가 ‘Dog’ 타입이 아니기 때문입니다.
2.2 instanceof의 주의점
1. null 검사 : ‘instanceof’ 는 객체 참조가 ‘null’ 일 때 항상 ‘false’ 를 반환합니다.
따라서 ‘null’ 값에 대한 추가적인 검사 없이도 안전하게 사용할 수 있습니다.
2. 다운캐스팅 검증 : 객체를 하위 클래스 타입으로 다운캐스팅하기 전에 ‘instanceof’ 를 사용하여 해당 객체가 실제로 해당 하위 클래스의 인스턴스인지를 확인하는 것이 안전합니다.
이를 통해 ‘ClassCastException’ 을 예발할 수 있습니다.
3. 인터페이스 검사 : ‘instanceof’ 는 클래스 뿐만 아니라 인터페이스 타입에 대해서도 사용할 수 있습니다. 객체가 특정 인터페이스를 구현하는지 여부를 검사할 수 있습니다.
📝 정리.
‘instanceof’ 는 다형성을 사용하는 객체 지향 프로그램에서 객체의 타입을 안전하게 확인하고, 타입에 맞는 적절한 동작을 수행하도록 도와주는 중요한 도구입니다.
3. 업캐스팅(Upcasting).
자바 프로그래밍에서 업캐스팅(Upcasting)은 서브클래스의 객체를 슈퍼클래스 타입의 참조로 변환하는 과정을 말합니다.
이는 일반적으로 자동으로 수행되며, 명시적으로 타입을 지정할 필요가 없습니다.
업캐스팅은 객체 지향 프로그래밍의 다형성을 활용하는 데 핵심적인 역할을 합니다.
3.1 업캐스팅의 특징과 이점.
1. 자동 형 변환 : 자바에서는 서브클래스의 객체를 슈퍼클래스 타입의 탐조 변수에 할당할 때 자동으로 업캐스팅이 발생합니다.
2. 안전성 : 업캐스팅은 항상 안전하며, 데이터 손실이나 오류 없이 수행됩니다. 이는 서브클래스가 슈퍼클래스의 모든 특성을 상속받기 때문입니다.
3. 다형적 행동 : 업캐스팅을 통해 서브클래스의 객체들을 슈퍼클래스 타입으로 다룰 수 있어, 다양한 타입의 객체들을 일관된 방식으로 처리할 수 있습니다. 이를 통해 코드의 유연성과 재사용성이 향상됩니다.
3.2 예시.
아래는 업캐스팅을 사용한 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
Animal myAnimal = myDog; // Dog 객체를 Animal 타입으로 업캐스팅
myAnimal.eat(); // 호출 가능
// myAnimal.bark(); // 컴파일 에러, Animal 타입은 bark 메소드를 알지 못함
}
}
이 예시에서 ‘Dog’ 객체가 ‘Animal’ 타입으로 업캐스팅 되었습니다.
‘myAnimal’ 변수는 ‘Animal’ 클래스의 메소드만 호출할 수 있으며, ‘Dog’ 클래스의 ‘bark()’ 메소드는 호출할 수 없습니다.
3.3 업캐스팅 후의 제한사항.
업캐스팅을 한 후에는 원래 서브클래스의 특정 메소드나 속성에 접근할 수 없게 됩니다.
즉, 업캐스팅된 객체는 슈퍼클래스의 필드와 메소드만 사용할 수 있으며, 추가된 서브클래스의 특성은 사용할 수 없습니다.
이는 다형성의 한 예로서, 슈퍼 클래스 타입을 통해 다양한 서브클래스의 객체들을 통합적으로 다룰 수 있도록 해주며, 프로그램을 더 유연하고 확장 가능하게 만듭니다.
4. 다운캐스팅(Downcasting).
자바 프로그래밍에서 다운캐스팅(Downcasting)은 슈퍼클래스 타입의 객체 참조를 서브클래스 타입의 참조로 변환하는 과정을 말합니다.
다운캐스팅은 업캐스팅의 반대 과정으로, 업캐스팅된 객체를 다시 원래의 서브클래스 타입으로 변환할 때 사용됩니다.
다운캐스팅은 명시적으로 수행되어야 하며, 자바에서는 이 과정이 자동으로 이루어지지 않습니다.
4.1 다운캐스팅의 필요성.
업캐스팅을 통해 객체가 슈퍼클래스 타입으로 변환되면, 해당 객체는 슈퍼클래스의 메소드와 필드만 접근 가능합니다.
서브클래스에만 있는 메소드나 필드에 접근하려면 다운캐스팅을 사용하여 해당 객체를 다시 서브클래스 타입으로 변환해야 합니다.
4.2 다운캐스팅의 사용법과 주의사항.
다운캐스팅은 타입 캐스팅 연산자를 사용하여 수행되며, 반드시 ‘instanceof’ 연산자로 타입 체크를 먼저 수행하는 것이 안전합니다.
이는 변환하려는 객체가 실제로 해당 서브클래스의 인스턴스인지 확인하여 ‘ClassCastExecption’ 을 방지하기 위함입니다.
4.3 예시.
다운캐스팅을 사용하는 자바 코드 예시입니다.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 업캐스팅
myAnimal.eat();
// 다운캐스팅 전에 instanceof로 체크
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 다운캐스팅
myDog.bark(); // 이제 서브클래스의 메소드 호출 가능
}
}
}
이 예시에서 ‘Animal’ 타입의 ‘myAnimal’ 객체는 ‘Dog’ 클래스의 인스턴스입니다.
‘myAnimal’ 을 ‘Dog’ 타입으로 다운캐스팅하여 ‘Dog’ 클래스의 ‘bark()’ 메소드에 접근할 수 있습니다.
다운캐스팅을 수행하기 전에 ‘instanceof’ 를 사용해 ‘myAnimal’ 이 실제로 ‘Dog’ 의 인스턴스인지 확인함으로써 안정을 확보합니다.
4.4 주의사항.
다운캐스팅은 객체의 실제 타입이 캐스팅하려는 클래스 타입과 일치할 때만 안전하게 수행됩니다.
잘못된 다운캐스팅은 런타임에 ‘ClassCastException’ 을 발생시킬 수 있습니다.
📝 정리.
다운캐스팅은 특정 상황에서 필수적이며, 객체의 모든 기능을 활용하기 위해 사용되지만, 항상 타입 검사를 수행하고 신중하게 사용해야 합니다.
-
☕️[Java] 추상클래스
1️⃣ 추상클래스.
추상 클래스가 무엇인지 설명할 수 있음
abstract를 이용하여 추상 클래스 구현
1. 추상 메소드(Abstract Method)
자바 프로그래밍에서 추상 메소드(abstract method)는 선언만 있고 구현은 없는 메소드입니다.
이러한 메소드는 추상 클래스(abstract class)나 인터페이스(interface) 내부에서 선언될 수 있으며, 구체적인 행동은 하위 클래스에서 구현됩니다.
추상 메소드를 사용하는 주된 목적은 하위 클래스가 특정 메소드를 반드시 구현하도록 강제하는 것입니다.
이는 코드의 일관성을 유지하고, 다형성을 통한 유연한 프로그래밍 설계를 가능하게 합니다.
1.2 추상 메소드의 특징.
선언만 있고 구현이 없음 : 메소드 본체가 없으며, 메소드 선언은 세미콜론(’;’) 으로 끝납니다.
하위 클래스에서의 구현 필수 : 추상 메소드를 포함하는 클래스를 상속받는 모든 하위 클래스는 해당 메소드를 구현해야만 인스턴스 생성이 가능합니다.
‘abstract’ 키워드 사용 : 메소드 앞에 ‘abstract’ 키워드를 명시하여 추상 메소드임을 표시합니다.
1.3 추상 메소드 예시
다음은 추상 클래스와 추상 메소드의 간단한 예시입니다.
abstract class Animal {
// 추상 메소드
abstract void makeSound();
void breathe() {
System.out.println("Btrathing...");
}
}
class Dog extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
// 추상 메소드 구현
void makeSound() {
System.out.println("Meow!");
}
}
위 예에서 ‘Animal’ 클래스는 ‘makeSound’ 라는 추상 메소드를 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 클래스는 ‘Animal’ 클래스를 상속받고 ‘makeSound’ 메소드를 각각 다르게 구현하고 있습니다.
이는 다형성의 좋은 예로, ‘Animal’ 타입의 참조를 사용하여 각각의 하위 클래스 객체를 다룰 때 동일한 메소드(‘makeSound’)를 호출하더라도 서로 다른 행동(개는 짖고, 고양이는 울음)을 보여줍니다.
1.4 결론.
추상 메소드는 프로그램의 확장성과 유지보수성을 향상시키는 객체 지향 설계의 핵심 요소입니다.
다양한 상황에 맞춰 동일한 인터페이스에 여러 구현을 제공할 수 있어 유연한 코드 작성이 가능합니다.
2. 추상 클래스(abstract class)
자바에서 추상 클래스(abstract class)는 완전하지 않은 클래스로, 추상 클래스 자체로는 인스턴스를 생성할 수 없습니다.
추상 클래스의 주요 목적은 다른 클래스들의 기본이 되는 클래스를 제공하여 코드의 재사용성을 높이고, 일관된 설계를 유도하는 것입니다.
추상 클래스는 하나 이상의 추상 메소드를 포함할 수 있으며, 또한 구현된 메소드도 포함할 수 있습니다.
2.1 추상 클래스의 특징.
1. 인스턴스 생성 불가 : 추상 클래스는 직접적으로 인스턴스를 생성할 수 없습니다. 반드시 상속을 통해 그 기능을 확장하고 구체적인 클래스에서 인스턴스를 생성해야 합니다.
2. 추상 메소드 포함 가능 : 추상 클래스는 하나 이상의 메소드를 포함할 수 있습니다. 추상 메소드는 선언만 있고 구현은 없으며, 이를 상속받은 구체적인 클래스에서 구현해야 합니다.
3. 구현된 메소드 포함 가능 : 추상 클래스는 구현된 메소드도 포함할 수 있어, 자식 클래스들이 이 메소드를 재사용하거나 오버라이드 할 수 있습니다.
4. 생성자 및 필드 포함 가능 : 추상 클래스는 자신의 생성자와 필드(변수)를 가질 수 있으며, 이는 상속받은 클래스에서 사용할 수 있습니다.
2.2 추상 클래스의 사용 예시.
abstract class Animal {
abstract void makeSound();
void eat() {
System.out.println("This animal is eating.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
이 예에서 ‘Animal’ 은 추상 클래스로, ‘makeSound()’ 메소드를 추상 메소드로 포함하고 있습니다.
‘Dog’ 와 ‘Cat’ 은 ‘Animal’ 클래스를 상속받아 ‘makeSound()’ 메소드를 각각 구현합니다.
추상 클래스 ‘Animal’ 의 ‘eat()’ 메소드는 모든 동물이 공통적으로 사용할 수 있는 구현된 메소드입니다.
2.3 결론.
추상 클래스는 공통적인 특징을 가진 클래스들 사이의 일반적인 행동을 정의하고, 이를 상속받는 구체적인 클래스들이 이를 구현하도록 하는 데에 주로 사용됩니다.
이를 통해 코드의 재사용성과 유지보수성을 향상시키며, 객체 지향 설계의 일관성과 안정성을 보장할 수 있습니다.
3. 익명 클래스(anonymous class).
자바에서 익명 클래스(anonymous class)는 이름이 없는 클래스입니다.
이들은 주로 일회성 사용 목적으로 설계되며, 인터페이스나 추상 클래스를 간편하게 구현하거나, 기존 클래스를 임시로 확장하기 위해 사용됩니다.
익명 클래스는 일반적으로 이벤트 리스너나 작은 콜백 객체 같이 간단한 기능을 수행하는 데에 활용됩니다.
3.1 익명 클래스의 특징.
1. 이름이 없음 : 익명 클래스는 이름을 가지지 않습니다. 인스턴스 생성 시점에 정의됩니다.
2. 즉석에서 정의 및 사용 : 익명 클래스는 즉석에서 정의되어 바로 인스턴스가 생성됩니다. 보통 이들은 한 번만 사용되고 재사용되지 않습니다.
3. 상속 및 구현 : 익명 클래스는 상쉬 클래스를 상속하거나 인터페이스를 구현할 수 있습니다. 그러나 다중 상속은 지원하지 않습니다.
4. 오직 하나의 인스턴스만 생성 가능 : 익명 클래스로부터 직접적으로 두 개 이상의 객체를 생성할 수는 없습니다. 다시 사용하려면 클래스 정의를 반복해야 합니다.
5. 지역 클래스 비슷 : 지역 변수처럼 동작하여 주변 스코프의 변수를 참조할 수 있습니다. 자바 8 이전에는 final 변수만 참조 가능했으나, 자바 8부터는 effectively final(명시적으로 final로 선언되지 않았어도 값이 변경되지 않는 변수) 변수도 참조할 수 있습니다.
3.2 익명 클래스의 사용 예.
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 클릭되었습니다!");
}
});
위 예제에서 ‘ActionListener’ 인터페이스는 익명 클래스를 통해 구현되었습니다.
‘button.addActionListener’ 메소드에 직접 전달되면서 버튼 클릭 시 “버튼이 클릭되었습니다!”를 출력하는 ‘actionPerformed’ 메소드를 포함하고 있습니다.
3.3 결론.
익명 클래스는 특정 인터페이스나 상위 클래스의 메소드를 구현하거나 오버라이드할 때 사용됩니다.
짧고 간단한 기능을 구현하는 데 유용하며, 코드의 간결성을 유지할 수 있게 도와줍니다.
하지만 복잡한 로직이나 반복적으로 사용될 기능에 대해서는 일반 클래스나 지역 클래스를 사용하는 것이 더 적합할 수 있습니다.
-
-
☕️[Java] 상속
1️⃣ 상속.
1. 상속(Inheritance)
자바 프로그래밍에서의 상속(Inheritance)은 한 클래스가 다른 클래스의 속성과 메소드를 물려받는 기능을 말합니다.
상속을 사용하면 기존 코드를 재사용하고 확장하는 것이 용이해져, 소프트웨어의 설계와 유지 보수가 효율적으로 이루어질 수 있습니다.
1.2 상속의 주요 개념.
1. 슈퍼클래스(부모 클래스) : 기능이 상속되는 클래스입니다.
예를 들어, ‘Vehicle’ 클래스가 있을 때 클래스의 속성(예: 속도)과 메소드(예: start, stop)를 다른 클래스가 상속받을 수 있습니다.
2. 서브클래스(자식 클래스) : 슈퍼클래스의 속성과 메소드를 상속받는 클래스입니다.
서브클래스는 메소드를 그대로 사용할 수도 있고, 필요에 따라 재정의(오버라이드)할 수도 있습니다.
예를 들어, ‘Car’ 클래스가 ‘Vehicle’ 클래스를 상속받는 경우, ‘Car’ 는 ‘Vehicle’ 의 모든 속성과 메소드를 사용할 수 있으며 추가적인 기능(예: 4륜 구동 기능)을 더할 수 있습니다.
3. 메소드 오버라이딩(Method Overriding) : 서브클래스가 슈퍼클래스에서 상속받은 메소드를 재정의하여 사용하는 것 입니다.
서브클래스는 상속받은 메소드를 자신의 필요에 맞게 변경할 수 있습니다.
4. 생성자 상속 : 자바에서 생성자는 상속되지 않습니다. 서브클래스의 생성자가 호출될 때, 슈퍼클래스의 생성자도 자동으로 호출되어야 하는데, 이는 ‘super()’ 키워드를 통해 명시적으로 호출해야 합니다.
📝 정리.
상속을 사용하면 코드의 중복을 줄이고, 각 클래스의 기능을 명확하게 구분지어 설계할 수 있어 프로그램 전체의 구조가 개선됩니다.
class 자식 클래스명 extends 부모 클래스명 { // 다중 상속 불가능
필드;
메소드;
...
}
2. 상속과 접근제어자와의 관계.
자바에서 상속과 접근 제어자(Access modifiers)는 클래스와 클래스 멤버(필드, 메소드)의 접근성을 결정하는 데 중요한 역할을 합니다.
접근 제어자는 클래스의 데이터를 보호하고, 코드의 유지 보수를 용이하게 하며, 외부로부터의 불필요한 접근을 막는 기능을 합니다.
상속에서 접근 제어자는 어떤 멤버가 서브클래스에게 상속될 수 있는지, 그리고 상속받은 멤버를 서브클래스가 어떻게 활용할 수 있는지 결정짓는 요소입니다.
2.1 주요 네 가지 접근 제어자.
1. private : 멤버가 선언된 클래스 내에서만 접근 가능합니다.
‘private’ 접근 제어자가 지정된 멤버는 상속되지 않습니다.
2. default(package-private) : 접근 제어자를 명시하지 않으면, 기본적으로 ‘default’ 접근이 적용됩니다.
이러한 멤버들은 같은 패키지 내의 다른 클래스에서 접근할 수 있지만, 다른 패키지의 서브클래스에서는 접근할 수 없습니다.
3. protected : ‘protected’ 멤버는 같은 패키지 내의 모든 클래스와 다른 패키지의 서브클래스에서 접근할 수 있습니다.
이 접근 제어자는 상속을 사용할 때 특히 유용하며, 서브클래스가 슈퍼클래스의 멤버를 활용하거나 수정할 수 있게 합니다.
4, public : ‘public’ 멤버는 모든 클래스에서 접근할 수 있습니다.
상속과 관련하여, ‘public’ 멤버는 서브클래스에 의해 자유롭게 상속되고 사용될 수 있습니다.
📝 정리.
상속과 접근 제어자의 관계에서 중요한 점은, 서브클래스가 상속받은 멤버에 접근할 수 있는 권한은 슈퍼클래스에서 해당 멤버에 지정된 접근 제어자에 의해 결정된다는 것 입니다.
예를 들어, 슈퍼클래스에서 ‘protected’ 로 선언된 메소드는 서브클래스에서 접근 가능하고 필요에 따라 오버라이딩할 수 있지만, ‘private’ 으로 선언된 메소드는 서브클래스에서 직접접으로 접근하거나 사용할 수 없습니다.
이러한 제한은 객체 지향 프로그래밍에서 캡슐화와 정보 은닉을 강화하는 데 도움을 줍니다.
3. super와 super().
자바에서 ‘super’ 키워드와 ‘super()’ 생성자 호출은 상속을 사용할 때 매우 중요한 역할을 합니다.
이들은 서브클래스가 슈퍼클래스와 상호작용할 수 있게 해 줍니다.
3.1 super와 super() 키워드의 사용 방식.
3.1.1 super 키워드.
‘super’ 키워드는 슈퍼 클래스의 필드나 메소드에 접근할 때 사용됩니다.
서브클래스에서 메소드를 어버라이드 했을 때, 슈퍼클래스의 버전을 호출하고 싶은 경우에 유용하게 사용할 수 있습니다.
이는 슈퍼클래스의 구현을 활용하면서 추가적인 기능을 서브클래스에 구현할 때 필요합니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 의 메소드 ‘start()’ 를 서브클래스 ‘Car’ 에서 오버라이드한 후, ‘Car’ 의 ‘start()’ 메소드에서 ‘super.start()’ 를 호출하면, ‘Vehicle’ 클래스의 ‘start()’ 메소드가 실행됩니다.
3.1.2 super() 생성자 호출.
‘super()’ 는 서브클래스의 생성자에서 슈퍼클래스의 생성자를 호출할 때 사용됩니다.
자바에서는 모든 클래스가 생성자를 가지며, 서브클래스의 생성자가 호출될 때 슈퍼클래스의 생성자도 자동으로 호출됩니다.
명시적으로 슈퍼클래스의 생성자를 호출하고자 할 때 ‘super()’ 를 사용합니다.
이 호출은 서브클래스의 생성자의 첫 번째 명령어로 위치해야 합니다.
슈퍼클래스의 생성자를 호출함으로써, 슈퍼 클래스의 인스턴스 변수들이 적절히 초기화될 수 있습니다.
예를 들어, 슈퍼클래스 ‘Vehicle’ 에 ‘Vehicle(int speed)’ 라는 생성자가 있고, 서브클래스 ‘Car’ 에서 이를 상속 받을 때, ‘Car’ 의 생성자에서 ‘super(100)’ 을 호출하면 ‘Vehicle’ 의 생성자가 호출죄어 ‘speed’ 변수가 ‘100’ 으로 초기화됩니다.
📝 정리.
이 두 사용법은 객체지향 프로그래밍에서 클래스의 계층을 통해 기능을 확장하고 관리하는 데 필수적입니다.
‘super’ 의 사용은 상속 관계에 있는 클래스 간의 코드를 재사용하고, 유지 관리를 쉽게 하며, 다형성을 구현하는 데 중요한 역할을 합니다.
4. 오버라이딩(Overriding)
자바 프로그래밍에서 오버라이딩(Overriding)은 서브클래스가 상속받은 슈퍼클래스의 메소드를 자신의 요구에 맞게 재정의하는 과정을 말합니다.
오버라이딩은 객체 지향 프로그래밍의 핵심 개념 중 하나로, 다형성을 가능하게 하며, 상속 받은 메소드를 서브클래스에서 새로운 방식으로 구현할 수 있도록 해줍니다.
4.1 오버라이딩 규칙.
오버라이딩을 할 때는 몇 가지 규칙을 따라야 합니다.
1. 메소드 이름과 시그니처 일치 : 오버라이딩할 메소드는 슈퍼클래스의 메소드와 동일한 이름, 매개변수 목록, 반환 타입을 가져야 합니다.
2. 접근 제어 : 오버라이딩하는 메소드는 슈퍼클래스의 메소드보다 더 제한적인 접근 제어를 가질 수 없습니다.
예를 들어, 슈퍼클래스의 메소드가 ‘public’ 이라면 서브클래스의 오버라이딩 메소드도 적어도 ‘public’ 이어야 합니다.
3. 반환 타입 : 오버라이딩하는 메소드의 반환 타입은 슈퍼클래스의 메소드 반환 타입과 같거나 그 하위 타입이어야 합니다.(이것은 공변 반환 타입이라고 함.)
4.2 오버라이딩의 예.
슈퍼클래스 ‘Animal’ 에 다음과 같은 메소드가 있다고 가정해 봅시다.
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
이제 ‘Dog’ 클래스가 ‘Animal’ 클래스를 상속받고 ‘makeSound()’ 메소드를 오버라이드하여 다음과 같이 구현할 수 있습니다.
public class Dog extends Animal {
@Override // 이 어노테이션은 선택적이지만, 오버라이딩임을 명시적으로 나타냅니다.
public void makeSound() {
System.out.println("Dog barks");
}
}
이 예에서 ‘Dog’ 클래스의 ‘makeSound()’ 메소드는 ‘Animal’ 의 makeSound() 메소드를 오버라이드하여 “Dog barks”를 출력하도록 재정의합니다.
4.3 오버라이딩의 중요성.
오버라이딩은 다음과 같은 이점을 제공합니다.
유연성 : 같은 메소드 호출이지만, 다양한 서브클래스에서 서로 다른 동작을 구현할 수 있습니다.
재사용성 : 기존의 코드를 변경하지 않고도, 상속받은 메소드를 새로운 요구에 맞게 확장할 수 있습니다.
유지보수 : 코드의 중복을 줄이고, 유지보수를 간편하게 할 수 있습니다.
📝 정리.
오버라이딩은 프로그램의 다형성을 구현하는 데 필수적인 기능으로, 상속받은 메소드를 사용하는 대신 서브클래스에 맞게 특화된 기능을 구현할 수 있도록 합니다.
-
-
☕️[Java] 클래스와 객체(2)
1️⃣ 클래스와 객체(2)
1. 오버로딩(Overloading).
자바 프로그래밍에서 오버로딩(Overloading)은 같은 클래스 내에서 메소드 이름이 같지만 매개변수의 타입이나 개수가 다른 여러 메소드를 정의하는 것을 의미합니다.
오버로딩을 사용하면 같은 기능을 하는 메소드라도 다양한 입력에 대응하여 유연하게 메소드를 호출할 수 있습니다.
오버로딩은 메소드만 가능하며, 생성자에도 적용될 수 있습니다.
1.2. 오버로딩의 규칙.
1. 메소드 이름이 같아야 합니다 : 오버로딩된 메소드들은 같은 이름을 공유합니다.
2. 매개변수 목록이 달라야 합니다 : 매개변수의 개수나 타입, 혹은 그 순서가 달라야 합니다. 매개변수의 차이를 통해 자바 컴파일러는 호출할 적절한 메소드를 결정합니다.
3. 반환 타입은 오버로딩을 구분하는 데 사용되지 않습니다 : 오버로딩된 메소드는 반환 타입이 다르더라도, 이는 오버로딩을 구분하는 데 사용되지 않습니다.
즉, 반환 타입만 다른 메소드는 오버로딩이 아닙니다.
4. 접근 제어자와 예외는 오버로딩을 구분하는 데 사용되지 않습니다 : 이 역시 메소드를 구별하는 데 사용되지 않습니다.
1.3. 오버로딩의 예.
public class Print {
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 타입
public void display(int a) {
System.out.println("Integer: " + a);
}
public void display(String a) {
System.out.println("String: " + a);
}
// 오버로딩 예제: 같은 메소드 이름, 다른 매개변수 개수
public void display(int a, int b) {
System.out.println("Two integers: " + a + ", " + b);
}
// 오버로딩 예제: 같은 메소드 이름, 매개변수의 순서가 다름
public void display(String a, int b) {
System.out.println("String and integer: " + a + ", " + b);
}
}
public class Test {
public static void main(String[] args) {
Print prt = new Print();
prt.display(1); // 출력: Integer: 1
prt.display("Hello"); // 출력: String: Hello
prt.display(1, 2); // 출력: Two integers: 1, 2
prt.display("Age", 30); // 출력: String and integer: Age, 30
}
}
이 예제에서 ‘Print’ 클래스는 ‘display’ 라는 메소드를 여러 번 오버로딩했습니다.
매개변수의 타입, 개수, 순서에 따라 다른 메소드가 호출됩니다.
이를 통해 다양한 타입과 개수의 입력을 유연하게 처리할 수 있습니다.
📝 정리.
오버로딩을 통해 프로그램의 가독성을 향상시키고, 유사한 기능을 하는 메소드들을 하나의 이름으로 그룹화함으로써 프로그램을 더욱 직관적으로 만들 수 있습니다.
이러한 방식은 프로그래밍의 복잡성을 줄이고, 코드의 유지보수를 용이하게 합니다.
2. 접근제어자(Access Modifiers).
자바 프로그래밍에서 접근 재어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접접으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
2.1 자바에서 사용하는 주요 접근 제어자
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언됩 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 냐의 클래스들만 접근할 수 있습니다. 이를 종종 package-private라고도 합니다.
private : 해당 멤보를 선언한 클래스 내에서만 접근할 수 있습니다. 외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
2.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protected int protectedVar = 20; // 같은 패키지 또는 상속 받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + pretectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
3. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 클래스의 멤버(필드, 메서드, 블록 또는 내부 클래스)를 클래스 레벨에 소속 시키는 역할을 합니다.
이는 특정 인스턴스에 속하기보다는 클래스 자체에 속한다는 의미입니다.
‘static’ 멤버는 클래스의 모든 인스턴스에 의해 공유되며, 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때 소멸됩니다.
3.1 static의 특징.
1. 클래스 레벨에서 공유 : ‘static’ 필드는 클래스의 모든 인스턴스 간에 공유됩니다.
이는 특정 데이터를 모든 객체가 공유해야 할 필요가 있을 때 유용합니다.
2. 인스턴스 생성 없이 접근 기는 : ‘static’ 메서드나 필드는 객체의 인스턴스를 생성하지 않고도 클래스 이름을 통해 직접 접근할 수 있습니다.
3. 정적 초기화 블록 : ‘static’ 키워드를 사용한 블록(정적 블록)은 클래스가 처음 메모리에 로그 될 때 단 한 번 실행됩니다.
이는 ‘static’ 필드의 초기화에 사용할 수 있습니다.
3.2 static 필드와 메서드 사용 예.
public class Calculator {
// 정적 필드
public static int calculatorCount = 0;
// 정적 블록
static {
System.out.println("Calculator 클래스 로딩!");
}
// 생성자
public Calculator() {
calculatorCount++; // 생성될 때마다 계산기의 수를 증가
}
// 정적 메서드
public static int add(int a, int b) {
return a + b;
}
}
public class Test {
public static void main(String[] args) {
Calculator c1 = new Calculator();
Calculator c2 = new Calculator();
System.out.println("Created Calculators: " + Calculator.calculatorCount); // 2 출력
System.out.println("Sum: " + Calculator.add(5,3)); // 8 출력
}
}
이 예제에서는 ‘Calculator’ 클래스의 인스턴스 생성 횟수를 추적하는 ‘static’ 필드 ‘calculatorCount’ 와 정적 메서드 ‘add’ 를 사용합니다.
‘calculatorCount’ 는 ‘Calculator’ 의 모든 인스턴스에 의해 공유되며, ‘add’ 메서드는 인스턴스를 생성하지 않고도 호출할 수 있습니다.
3.3 static 사용 시 주의점
‘static’ 메서드 내에서는 인스턴스 변수나 메서드를 직접 사용할 수 없습니다.
‘static’ 은 남용하면 객체지향의 원칙을 해칠 수 있습니다.
예를 들어, 객체 간의 상태 공유가 과도하게 이루어져 객체 간의 결합도가 높아질 수 있습니다.
‘static’ 변수는 프로그램의 실행이 끝날 때까지 메모리에 남아 있으므로 메모리 사용에 주의해야 합니다.
3.4 static 메소드와 static 변수와의 관계성.
자바 프로그래밍에서 ‘static’ 메소드와 ‘static’ 변수는 두 가지 공통점을 가지고 있습니다.
둘 다 클래스 레벨에서 정의되며, 클래스의 모든 인스턴스 간에 공유됩니다.
이런 공통점 때문에, ‘static’ 메소드는 ‘static’ 변수에 직접 접근할 수 있지만, 일반 인스턴스 변수에는 접근할 수 없습니다.
3.5 static 변수.
‘static’ 변수는 클래스 변수라고도 하며, 특정 클래스의 모든 인스턴스에 의해 공유됩니다.
이 변수는 클래스가 메모리에 로드될 때 생성되고, 클래스가 언로드될 때까지 메모리에 존재합니다.
‘static’ 변수는 특히 클래스의 인스턴스들이 공통적으로 사용해야 하는 데이터를 저장하는데 유용합니다.
예를 들어, 모든 계산기 객체가 공유해야 하는 ‘calculatorCount’ 와 같은 경우에 사용됩니다.
3.6 static 메소드
‘static’ 메소드 역시 클래스 레벨에 정의되며, 이 메소드는 인스턴스 생성 없이 클래스 이름을 통해 직접 호출할 수 있습니다.
‘static’ 메소드는 인스턴스 필드나 메소드에 접근할 수 없습니다.
그 이유는 ‘static’ 메소드가 호출될 때 해당 클래스의 인스턴스가 존재하지 않을 수 있기 때문입니다.
따라서 ‘static’ 메소드는 오로지 ‘static’ 변수나 다른 ‘static’ 메소드에만 접근할 수 있습니다.
3.7 두 요소의 상호작용.
‘static’ 메소드에는 ‘static’ 변수에 자유롭게 접근하고 수정할 수 있습니다.
이는 ‘static’ 변수가 클래스에 속하고 메소드도 클래스 레벨에서 실행되기 때문입니다.
예를 들어, 어떤 클래스의 모든 인스턴스가 사용할 설정 값을 ‘static’ 변수에 저장하고, 이 값을 설정하거나 조회하는 ‘static’ 메소드를 제공할 수 있습니다.
3.8 예제
public class Counter {
public static int count = 0; // 'static' 변수
public static void increment() { // 'static' 메소드
count++; // 'static' 변수에 접근하여 값을 증가
}
public static void displayCount() {
System.out.println("Count: " + count); // 'static' 변수의 현재 값을 출력
}
}
public class Test {
public static void main(String[] args) {
Counter.increment();
Counter.increment();
Countet.displayCount(); // 출력: Count: 2
}
}
이 예제에서 ‘Counter’ 클래스는 ‘static’ 변수 ‘count’ 를 가지고 있으며, increment 메소드를 통해 이 변수의 값을 증가시키고, ‘displayCount’ 메소드를 통해 값을 출력합니다.
모든 ‘Counter’ 객체가 ‘count’ 값을 공유하며, ‘static’ 메소드를 통해 이 값을 조작할 수 있습니다.
📝 정리.
‘static’ 은 전역 변수나 전역 메서드와 유사한 효과를 제공하지만, 자바의 객체지향적 특성과 일관성을 유지하기 위해 적절히 사용되어야 합니다.
‘static’ 메소드와 ‘static’ 변수는 클래스 레벨에서 관리되어 클래스의 모든 인스턴스에 의해 공유되는 특성을 가지고 있습니다.
이를 통해 클래스 전체에 영향을 미치는 작업을 수행할 수 있습니다.
-
-
☕️[Java] 다차원 배열
1️⃣ 다차원 배열.
자바 프로그래밍에서 다차원 배열이란, 배열의 배열을 의미합니다.
이는 데이터를 행렬이나 그리드 형태로 구성할 수 있게 해주며, 주로 2차원 이상의 데이터 구조를 필요로 할 때 사용됩니다.
가장 흔한 형태는 2차원 배열이지만, 3차원 이상의 배열도 만들 수 있습니다.
1. 2차원 배열.
2차원 배열은 행렬과 비슷하게 생각할 수 있으며, 각 행과 열에 데이터를 저장합니다.
예를 들어, 숫자로 이루어진 표를 저장하거나 정보를 격자 형태로 관리할 때 유용합니다.
1.2 2차원 배열의 초기화
자바에서 이차원 배열을 초기화하는 방법은 크게 세 가지로 나눌 수 있습니다.
배열을 선언할 때 크기만 지정해 두거나, 선언과 동시에 특정 값을 사용하여 초기화하거나, 나중에 각 요소에 값을 할당할 수 있습니다.
아래는 각 방법에 대한 설명과 예제입니다.
1.1.1 크기만 지정하여 배열 선언하기.
이 방법은 배열의 행과 열의 크기를 지정해 초기화하지만, 배열의 각 요소는 자동으로 기본 값으로 설정됩니다.(예: int의 경우 0).
inu[][] array = new int[3][4]; // 3행 4열의 배열 생성
이렇게 선언된 배열은 모든 요소가 0으로 초기화됩니다.
1.1.2 선언과 동시에 초기값을 제공하여 배열 초기화하기.
배열을 선언하면서 동시에 초기값을 제공할 수 있습니다.
이 방법은 배열의 내용을 명확히 알고 있을 때 유용합니다.
int[][] array = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}; // 각 행에 대한 값들을 중괄호로 묶어서 초기화
이 예제에서 배열은 3행 4열의 구조로, 각 행의 값이 명시적으로 초기화되어 있습니다.
1.1.3 반복문을 사용하여 배열 초기화하기.
반복문을 사용하면 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 런타임에 따라 배열 값을 설정해야 할 때 유용합니다.
int[][] array = new int[3][4];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
array[i][j] = (i + 1) * (j + 1); // 각 요소를 행 인덱스와 열 인덱스의 곱으로 초기화
}
}
이 방법은 배열의 각 위치에 i와 j 인덱스에 의존하는 계산 결과를 저장합니다.
📝 정리.
이 세 가지 방법은 상황에 따라 각기 다른 이점을 제공하므로, 요구 사항에 맞게 선택하여 사용할 수 있습니다.
2. 3차원 배열.
3차원 배열은 데이터를 3차원 공간으로 구성하여 저장합니다.
이는 비디오 게임의 공간 데이터, 과학 실험 데이터 등 복잡한 정보를 구조화하는 데 사용될 수 있습니다.
2.1. 3차원 배열의 초기화.
자바에서 3차원 배열을 초기화하는 방법은 2차원 배열과 유사합니다.
크게 세 가지 방법으로 나눌 수 있습니다.
크기만 지정하여 선언하기.
선언과 동시에 구체적인 값으로 초기화하기
반복문을 사용하여 동적으로 초기화하기
아해는 각 방법에 대한 설명과 예시입니다.
2.1.1. 크기만 지정하여 배열 선언하기.
이 방법은 삼차원 배열의 각 차원의 크기를 지정합니다.
각 요소는 자동으로 기본값으로 설정됩니다.(예: ‘int’ 의 경우 0).
int[][][] array = new int[3][4][5] // 3개의 4x5 행렬을 갖는 삼차원 배열
이 배열은 3개의 2차원 배열을 가지며, 각 2차원 배열은 4행 5열 구조입니다.
2.1.2. 선언과 동시에 초기값을 제공하여 배열 초기화하기.
삼차원 배열을 선언하면서 바로 값을 지정할 수 있습니다.
이 방법은 각 요소의 초기값을 명확히 알고 있을 때 매우 유용합니다.
int [][][] array = {
{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15},
{16, 17, 18, 19, 20}
},
{
{21, 22, 23, 24, 25},
{26, 27, 28, 29, 30},
{31, 32, 33, 34, 35},
{36, 37, 38, 39, 40}
},
{
{41, 42, 43, 44, 45},
{46, 47, 48, 49, 50},
{51, 52, 53, 54, 55},
{56, 57, 58, 59, 60}
}
}; // 각 행렬 및 행에 대한 값들을 중괄호로 묶어서 초기화
2.1.3. 반복문을 사용하여 배열 초기화하기
반복문을 사용해 삼차원 배열의 각 요소를 동적으로 초기화할 수 있습니다.
이 방법은 프로그램 실행 중에 배열 값을 설정해야 할 때 매우 유용합니다.
int[][][] array = new int[3][4][5];
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
for (int k = 0; k < array[i][j].length; k++) {
array[i][j][k] = (i + 1) * (j + 1) * (k + 1) // 각 요소를 i, j, k 인덱스의 곱으로 초기화
}
}
}
이 예제에서는 각 위치에 해당하는 인덱스의 곱을 저장하여 배열을 초기화하고 있습니다.
이러한 초기화 방법은 특히 배열의 구조가 복잡할 때 배열을 효과적으로 관리할 수 있게 도와줍니다.
-
☕️[Java] 클래스와 객체(1)
1️⃣ 클래스와 객체(1)
1. 클래스(Class)
자바 프로그래밍 언어에서 클래스는 객체를 생성하기 위한 설계도 혹은 템플릿입니다.
클래스는 객체의 상태를 정의하는 필드(변수)와 객체의 행동을 정의하는 메서드(함수)로 구성됩니다.
클래스를 사용하는 주된 목적은 데이터와 그 데이터를 조작하는 방법들은 하나의 장소에 묶어 관리하기 위함입니다.
이를 통해 데이터 추상화, 캡슐화, 상속, 다형성 등의 객체지향 프로그래밍의 주요 개념들을 구현할 수 있습니다.
1.2 클래스의 구성 요소.
1. 필드(Field) : 객체의 데이터 또는 상태를 저장하는 변수입니다.
2. 메서드(Method) : 객체가 수행할 수 있는 행동을 정의한 코드 블록입니다.
메서드는 필드의 값을 처리하거나 다른 메서드를 호출할 수 있습니다.
3. 생성자(Constructor) : 클래스로부터 객체를 생성할 때 초기화를 담당하는 특별한 종류의 메서드입니다.
생성자는 클래스 이름과 같은 이름을 가집니다.
1.3 클래스 예제
자바에서의 간단한 클래스 예제를 살펴보겠습니다.
public class Car {
// 필드(변수)
private String color;
private String model;
// 생성자
public Car(String color, String model) {
this.color = color;
this.model = model;
}
// 메서드
public void drive() {
System.out.println(model + " 색상의 " + color + " 자동차가 주행 중입니다.");
}
}
// 객체 생성 및 사용
public class Test {
public static void main(String[] args) {
Car myCar = new Car("레드", "테슬라");
myCar.drive();
}
}
위의 예제에서 ‘Car’ 클래스는 ‘color’와 ‘model’이라는 두 개의 필드를 가지며, 이는 각각 자동차의 색상과 모델을 나타냅니다.
‘Car’ 클래스의 객체를 생성할 때는 ‘new’ 키워드와 함께 생성자를 호출하여 초기 상태를 설정합니다.
‘drive’ 메서드는 자동차가 주행하고 있음을 시뮬레이션하는 기능을 합니다.
📝 정리.
클래스를 사용함으로써 코드의 재사용성, 관리성 및 확장성이 향상되며, 대규모 소프트웨어 개발에서 필수적인 요소가 됩니다.
2. 객체(Object)와 인스턴스(Instance).
자바 프로그래밍에서 “객체(Object)”와 “인스턴스(Instance)”는 매우 중요한 개념입니다.
이 두 용어는 종종 서로 바꿔 쓰이지만, 각각의 의미에는 약간의 차이가 있습니다.
2.1 객체(Object).
객체는 소프트웨어 세계의 구성 요소로, 실제 세계의 객체를 모방한 것입니다.
객체는 데이터(속성)와 그 데이터를 조작할 수 있는 함수(메서드)를 캡슐화합니다.
객체는 클래스에 정의된 속성과 기능을 실제로 사용할 수 있도록 메모리상에 할당된 구조입니다.
객체의 개념은 클래스의 특성을 실제로 구현하는 것입니다.
2.2 인스턴스(Instance).
인스턴스는 클래스 타입에 따라 생성된 객체를 의미합니다.
예를 들어, ‘Car’ 클래스의 구체적인 객체(예: 빨간색 테슬라 자동차, 파란색 현대 자동차 등)는 모두 ‘Car’ 클래스의 인스턴스입니다.
즉, 인스턴스는 특정 클래스의 구현체입니다.
인스턴스라는 용어는 주로 객체가 메모리에 할당되어 실제로 생성되었음을 강조할 때 사용됩니다.
2.3 객체와 인스턴스의 관계.
간단히 말해, 모든 인스턴스는 객체입니다, 하지만 사용된 맥락에 따라 ‘인스턴스’라는 용어는 그 객체가 특정 클래스의 구현체임을 명시적으로 나타낼 때 사용됩니다.
예를 들어, 우리가 ‘new Car(“blue”, “Hyundai”)’ 를 통해 생성한 객체는 ‘Car’ 클래스의 인스턴스입니다.
2.4 예제 코드.
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " makes a noise.");
}
}
public class Test {
public static void main(String[] args) {
// 여기서 'dog'는 Animal 클래스의 객체이자 인스턴스입니다.
Animal dog = new Animal("Dog");
dog.speak();
}
}
위 예제에서 ‘Animal’ 클래스가 있고, ‘main’ 메서드에서 ‘Animal’ 클래스의 새 객체를 생성합니다.
여기서 ‘dog’ 는 ‘Animal’ 클래스의 인스턴스이며 객체입니다.
‘dog’ 는 ‘Animal’ 클래스에 정의된 메서드와 필드를 사용할 수 있습니다.
📝 정리.
요약하면, 객체는 속성과 메서드를 갖는 소프트웨어의 기본 구성 단위이고, 인스턴스는 그 객체가 특정 클래스의 실제 구현체임을 의미합니다.
이 두 용어는 프로그래밍에서 클래스 기반의 객체를 생성하고 다룰 때 핵심적인 역할을 합니다.
클래스와 객체의 관계를 이해
기본 사용 방법과 생성자 및 this의
3. 메소드(Method).
자바 프로그래밍에서 메소드(Method)는 클래스에 속한 함수로서, 특정 작업을 수행하는 코드 블록입니다.
메소드는 객체의 행동을 정의하며, 클래스 내에서 정의된 데이터나 상태(필드)를 조작하는 데 사용됩니다.
메소드를 통해 객체지향 프로그래밍의 중요한 특징인 캡슐화와 추상화를 구현할 수 있습니다.
3.1 메소드의 주요 특징.
1. 재사용성 : 메소드는 코드의 재사용성을 증가시킵니다. 한 번 정의된 메소드는 여러 위치에서 호출되어 사용될 수 있습니다.
2. 모듈성 : 메소드를 사용함으로써 큰 프로그램을 작은 단위로 나누어 관리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시킵니다.
3. 정보 은닉 : 메소드를 통해 구현 세부사항을 숨기고 사용자에게 필요한 기능만을 제공할 수 있습니다.
3.2 메소드의 구성 요소.
1. 메소드 이름 : 메소드를 식별하는 데 사용되며, 메소드가 수행하는 작업을 설명하는 명확한 이름을 가집니다.
2. 매개변수 목록(Parameter List) : 메소드에 전달되는 인자의 타입, 순서, 그리고 개수를 정의합니다. 매개변수는 선택적일 수 있습니다.
3. 반환 타입 : 메소드가 작업을 수행한 후 반환하는 데이터의 타입입니다. 반환할 데이터가 없으면 ‘void’ 로 지정됩니다.
4. 메소드 바디 : 실제로 메소드가 수행할 작업을 구현하는 코드 블록입니다.
3.3 예제.
자바에서 간단한 메소드 예제를 보여드리겠습니다.
public class Calculator {
// 메소드 정의: 두 정수의 합을 반환
public int add(int num1, int num2) {
return num1 + num2;
}
// 메소드 정의: 두 정수의 차를 반환
public int subtract(int num1, int num2) {
return num1 - num2;
}
}
public class Test {
public static void main(String[] args) {
Calculator calc = new Calculator();
int result1 = calc.add(5, 3); // 8 반환
int result2 = calc.subtract(5, 3); // 2 반환
System.out.println("Addition Result: " + result1);
System.out.println("Subtraction Result: " + result2);
}
}
이 예제에서 ‘Calculator’ 클래스는 두 개의 메소드 ‘add’ 와 ‘subtract’ 를 가지고 있습니다.
각각의 메소드는 두 개의 정수를 받아 그 결과를 반환합니다.
이렇게 메소드를 사용하면 코드를 효율적으로 관리할 수 있으며, 필요에 따라 재사용할 수 있습니다.
📝 정리.
메소드는 자바 프로그래밍에서 기능을 모듈화하고 코드의 재사용을 가능하게 하는 핵심 요소입니다.
4. 접근 제어자(Access Modifiers)
자바 프로그래밍에서 접근 제어자(Access Modifiers)는 클래스, 메서드, 변수 등과 같은 멤버들에 대한 접근 권한을 제어하는 키워드입니다.
이러한 접근 제어자를 사용함으로써 클래스의 캡슐화를 강화할 수 있으며, 객체의 데이터와 메서드를 외부에서 직접적으로 접근하거나 수정하는 것을 제한할 수 있습니다.
접근 제어자는 클래스의 멤버(변수, 메서드, 생성자 등)와 클래스 자체에 적용될 수 있습니다.
4.1 자바에서 사용하는 주요 접근 제어자.
1. public : 어떤 클래스에서든 접근할 수 있도록 허용합니다.
public으로 선언된 멤버는 어디서든 접근이 가능합니다.
2. protected : 같은 패키지 내의 클래스 또는 다른 패키지의 서브 클래스에서 접근할 수 있습니다.
3. default(package-private) : 접근 제어자를 명시하지 않은 경우, 같은 패키지 내의 클래스들만 접근할 수 있습니다.
이를 종종 package-private라고도 합니다.
4. private : 해당 멤버를 선언한 클래스 내에서만 접근할 수 있습니다.
외부 클래스에서는 접근할 수 없어, 클래스 내부 구현을 숨기는 데 유용합니다.
4.2 접근 제어자의 사용 예제.
public class AccessExample {
public int publicVar = 10; // 어디서든 접근 가능
protexted int protectedVar = 20; // 같은 패키지 또는 상속받은 클래스에서 접근 가능
int defaultVar = 30; // 같은 패키지 내에서만 접근 가능
private int privateVar = 40; // 이 클래스 내에서만 접근 가능
public void show() {
System.out.println("publicVar: " + publicVar);
System.out.println("protectedVar: " + protectedVar);
System.out.println("defaultVar: " + defaultVar);
System.out.println("privateVar: " + privateVar);
}
}
public class Test {
public static void main(String[] args) {
AccessExample example = new AccessExample();
System.out.println(example.publicVar); // 접근 가능
System.out.println(example.protectedVar); // 다른 패키지에 있지 않은 이상 접근 가능
System.out.println(example.defaultVar); // 같은 패키지에 있을 경우 접근 가능
// System.out.println(example.privateVar); // 컴파일 에러 발생, 접근 불가능
example.show(); // 모든 변수 출력 가능
}
}
위 예제에서는 다양한 접근 제어자가 적용된 변수들을 선언하고, 이에 대한 접근 가능성을 보여줍니다.
‘publicVar’ 은 어디서든 접근할 수 있지만, ‘privateVar’ 는 오직 선언된 클래스 내부에서만 접근할 수 있습니다.
‘protectedVar’ 과 ‘defaultVar’ 는 좀 더 제한적인 접근을 허용합니다.
📝 정리.
이렇게 접근 제어자를 통해 자바에서는 데이터 보호 및 캡슐화, 객체의 정확한 사용을 보장하여 프로그램의 안정성과 유지보수성을 향상시킬 수 있습니다.
5. static 키워드.
자바 프로그래밍에서 ‘static’ 키워드는 특정 필드나 메소드, 또는 중첩 클래스를 클래스의 인스턴스가 아닌 클래스 자체에 소속되게 합니다.
이를 사용함으로써 해당 멤버는 클래스의 모든 인스턴스에 걸쳐 공유되며, 인스턴스 생성 없이 클래스 이름을 통해 직접 접근할 수 있습니다.
5.1 static의 주요 사용 사례.
1. 정적 필드(Static Fields) : 모든 인스턴스에 의해 공유되는 클래스 변수입니다.
예를 들어, 회사의 모든 직원이 같은 회사 이름을 공유할 때 사용할 수 있습니다.
2. 정적 메소드(Static Methods) : 인스턴스 변수에 접근할 필요 없이, 클래스 이름을 통해 직접 호출할 수 있는 메소드입니다.
유틸리티 함수나 핼퍼 함수를 작성할 때 자주 사용됩니다.
3. 정적 초기화 블록(Static Initialization Blocks) : 클래스가 처음 로딩될 때 한 번 실행되며, 정적 변수를 초기화하는 데 사용됩니다.
4. 정적 중첩 클래스(Static Nested Classes) : 다른 클래스 내부에 위치하면서도 독립적으로 사용될 수 있는 클래스입니다.
5.2 static 키워드의 장점과 단점.
장점.
메모리 효율성 : static 멤버는 클래스 로드 시 메모리에 한 번만 할당되고 모든 인스턴스가 공유하기 때문에 메모리 사용을 최소화할 수 있습니다.
편리성 : 객체 생성 없이 바로 접근할 수 있어, 유틸리티 함수 같은 공통 기능 구현에 유용합니다.
단점.
과도한 사용은 객체지향 원칙에 어긋남 : 객체 간의 결합도가 높아지고, 객체의 상태 관리가 어려워질 수 있습니다.
테스트가 어려워질 수 있슴 : static 메소드는 오버라이드가 불가능하며, 상태를 공유하기 때문에 병렬 테스트 환경에서 문제를 일으킬 수 있습니다.
5.3 예제.
public class Company {
// 정적 필드
public static String companyName = "Global Tech";
// 정적 메소드
public static void printCompanyName() {
System.out.println("Company Name: " + companyName);
}
}
public class Test {
public static void main(String[] args) {
// 객체 생성 없이 정적 메소드 호출
Company.printCompanyName();
}
}
이 예제에서 ‘Company’ 클래스에는 정적 필드 ‘companyName’ 과 정적 메소드 ‘printCompanyName()’ 이 있습니다.
‘main’ 메소드에서는 ‘Company’ 클래스의 객체를 생성하지 않고도 ‘printCompanyName()’ 메소드를 호출하려 회사 이름을 출력합니다.
📝 정리.
정적 멤버는 클래스와 관련된, 변하지 않는 값이나, 모든 인스턴스가 공유해야 하는 정보를 관리할 때 유용하게 사용됩니다.
6. 생성자(Constructor)
자바 프로그래밍에서 생성자(Constructor)는 클래스로부터 객체가 생성될 때 호출되는 특별한 유형의 메서드입니다.
생성자의 주요 목적은 새로 생성된 객체를 초기화하는 것으로, 객체의 기본 상태를 설정하는 데 사용됩니다.
생성자는 메서드처럼 보일 수 있지만, 리턴 타입이 없고 클래스 이름과 동일한 이름을 가집니다.
6.1 생성자 특징.
1. 클래스 이름과 동일 : 생성자의 이름은 항상 선언된 클래스의 이름과 동일해야 합니다.
2. 리턴 타입 없음 : 생성자는 값을 반환하지 않으며, 리턴 타입도 선언하지 않습니다.
3. 자동 호출 : 객체가 생성될 때 자동으로 호출됩니다.
이는 객체의 필드를 초기화하거나, 객체 생성 시 실행해야 할 다른 시작 루틴을 실행하는 데 사용할 수 있습니다.
4. 오버로딩 가능 : 하나의 클래스에 여러 생성자를 정의할 수 있습니다.
이를 생성자 오버로딩이라고 하며, 파라미터의 수나 타입에 따라 다른 생성자를 호출할 수 있습니다.
6.2 생성자의 유형.
1. 기본 생성자(Default Constructor) : 개발자가 명시적으로 생성자를 정의하지 않으면, 자바 컴파일러는 매개변수가 없는 기본 생성자를 제공합니다.
이 기본 생성자는 객체의 필드를 기본값으로 초기화합니다.
2. 매개변수가 있는 생성자(Parameterized Constructor) : 하나 이상의 매개변수를 받아 객체의 초기 상태를 세팅 할 수 있도록 해줍니다.
6.3 예제.
public class Person {
private String name;
private int age;
// 기본 생성자
public Person() {
this.name = "Unknown";
this.age = 0;
}
// 매개변수가 있는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class Test {
public static void main(String[] args) {
// 기본 생성자를 사용하여 객체 생성
Person person1 = new Person();
person1.displayInfo(); // 출력: Name: Unknown, Age: 0
// 매개변수가 있는 생성자를 사용하여 객체 생성
Person person2 = new Person("Jhon", 25);
person2.displayInfo(); // 출력: Name: Jhon, Age: 25
}
}
이 예제에서 ‘Person’ 클래스는 두 가지 유형의 생성자를 가집니다.
하나는 매개변수가 없어 기본값으로 객체를 초기화하고, 다른 하나는 이름과 나이를 받아 객체를 초기화합니다.
📝 정리.
생성자를 사용함으로써 클래스의 인스턴스가 유효한 상태로 시작될 수 있도록 보장하며, 필요한 초기 설정을 자동으로 수행할 수 있습니다.
이는 객체 지향 프로그래밍에서 객체의 무결성을 유지하는 중요한 방법입니다.
7. this 키워드와 this() 생성자 호출.
자바에서 ‘this’ 키워드와 ‘this()’ 생성자 호출은 객체 자신을 참조하고 객체의 생성자를 호출하는 데 사용되는 중요한 요소입니다.
이들은 객체 내부에서 사용되며, 클래스의 멤버(필드, 메서드, 생성자)와 관련된 동작을 명확히 하는 데 유용합니다.
7.1 this 키워드.
‘this’ 키워드는 현재 객체, 즉 메서드나 생성자를 호출하는 인스턴스를 참조하는 데 사용됩니다.
주로 다음과 같은 상황에서 사용됩니다.
1. 필드와 매개변수 이름이 같을 때 구분 : 메서드나 생성자의 매개변수와 클래스의 필드 이름이 같을 때, 필드와 매개변수를 구분하기 위해 사용됩니다.
2. 메서드 체이닝 : 객체의 메서드를 연속적으로 호출할 때 ‘this’ 를 반환함으로써 메서드 체이닝을 구현할 수 있습니다.
3. 현재 객체를 다른 메서드에 전달 : 현재 객체의 참조를 다른 메서드에 전달할 때 사용됩니다.
7.2 this() 생성자 호출.
‘this()’ 는 같은 클래스의 다른 생성자를 호출하는 데 사용됩니다.
주로 생성자 오버로딩이 있을 때, 중복 코드를 최소화하고, 하나의 생성자에서 다른 생성자를 호출하여 필드 초기화 등의 공통 작업을 중앙집중적으로 관리할 수 있게 해줍니다.
1. 생성자 오버로딩 처리 : 클래스에 여러 생성자가 있을 때, ‘this()’ 를 사용하여 한 생성자에서 다른 생성자를 호출함으로써 공통 코드를 재사용할 수 있습니다.
2. 코드 간결성 유지 : 필수적인 초기화 작업을 주 생성자에만 명시하고, 나머지 생성자는 이 주 생성자를 호출하게 함으로써 코드의 간결성을 유지합니다.
7.3 예제.
public class Rectangle {
private int width;
private int height;
// 주 생성자
public Rectangle(int width, int height) {
this.width = width; // 'this'로 필드와 매개변수 구분
this.height = height;
}
// 부 생성자
public Rectangle() {
this(10, 10) // 'this()' 로 다른 생성자 호출
}
public void displaySize() {
System.out.println("Width: " + this.width + ", Height: " + this.height);
}
}
public class Test {
public static void main(String[] args) {
Rectangle rect1 = new Rectangle(30, 40);
rect1.displaySize(); // 출력: Width: 30, Height: 40
Rectangle rect2 = new Rectangle();
rect2.displaySize(); // 출력: Width: 10, Height: 10
}
}
이 예제에서 ‘Rectangle’ 클래스는 두 개의 생성자를 가지고 있습니다.
기본 생성자는 ‘this()’ 를 사용하여 주 생성자를 호출하고, 주 생성자에서는 ‘this’ 키워드를 사용하여 클래스 필드와 생성자 매개변수를 구분합니다.
이렇게 ‘this’ 와 ‘this()’ 를 사용함으로써 코드의 중복을 줄이고, 초기화 로직을 하나의 생성자에 집중할 수 있습니다.
📝 정리.
이처럼 ‘this’ 와 ‘this()’ 는 자바에서 클래스의 인스턴스 자신을 참조하거나 클래스 내 다른 생성자를 호출하는 데 매우 유용한 도구입니다.
-
-
☕️[Java] 반복문
1️⃣ 반복문
1. for 반복문.
자바 프로그래밍에서 ‘for’ 반복문은 특정 조건을 만족하는 동안 코드 블록을 반복해서 실행하도록 설계된 제어 구조입니다.
‘for’ 문은 초기화, 조건 검사, 반복 후 실행할 작업(일반적으로 증감)을 한 줄에 명시하여 코드의 가독성과 관리를 용이하게 합니다.
이는 반복 실행이 필요한 많은 상황에서 유용하게 사용됩니다.
1.2 for 반복문의 기본 구조.
‘for’ 문의 기본 구조는 다음과 같습니다.
for (초기화; 조건; 증감) {
// 반복해서 실행할 코드
}
초기화 : 반복문이 시작할 때 한 번 실행되는 부분으로, 반복문의 제어 변수를 초기 설정합니다.
조건 : 이 조건이 참(‘true’) 인 동안 반복문 내의 코드가 실행 됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
증감 : 각 반복의 끝에서 실행되며, 주로 제어 변수의 값을 증가시키거나 감소시키는데 사용됩니다.
1.3 for 반복문의 예시
기본 예시
for (int i = 0; i < 5; i++) {
System.out.println("i의 값은: " + i);
}
이 예제에서는 ‘i’ 를 0부터 시작하여 ‘i’ 가 5미만일 동안 반복하여, 매 반복마다 ‘i’ 를 1씩 증가시킵니다.
따라서 “i의 값은: 0” 부터 “i의 값은: 4” 까지 총 다섯 번의 출력을 하게 됩니다.
확장된 예시: 다중 제어 변수
for (int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i = " + i + ". j = " + j);
}
이 예제에서는 두 개의 제어 변수 ‘i’ 와 ‘j’ 를 사용합니다.
‘i’ 는 증가하고 ‘j’ 는 감소하며, ‘i’ 가 ‘j’ 와 같거나 ‘j’ 보다 크게 되면 반복이 종료됩니다.
이런 패턴은 복잡한 반복 조건이 필요한 경우 유용하게 사용됩니다.
1.4 사용 사례
‘for’ 반복문은 배열이나 컬렉션과 같은 데이터 구조를 순회할 때 매우 유용합니다.
예를 들어, 배열의 모든 요소를 처리하거나 조작할 때 자주 사용됩니다.
int[] numbers = {1,2,3,4,5};
for (int i = 0; i < numbers.length; i++) {
System.out.println("배열 요소: " + numbers[i]);
}
여기서 ‘numbers.length’ 는 배열의 길이를 반환하며, ‘i’ 는 0에서 시작하여 배열의 크기 미만이 될 때까지 증가하면서 배열의 각 요소에 접근합니다.
📝 정리
‘for’ 반복문은 코드를 간결하게 하면서 반복적인 작업을 효과적으로 처리할 수 있도록 도와줍니다.
2. while 반복문.
자바 프로그래밍에서 ‘while’ 반복문은 특정 조건이 참(‘true’)인 동간 주어진 코드 블록을 반복적으로 실행하는 구조 입니다.
‘while’ 문은 주로 반복 횟수가 불확실할 때 또는 반복 횟수를 사전에 정확히 알 수 없을 때 사용됩니다.
2.1 while 반복문의 기본 구조.
‘while’ 문의 기본 구조는 다음과 같습니다.
while (조건) {
// 조건이 참인 동안 반복 실행될 코드
}
여기서 ‘조건’ 은 각 반복 이전에 평가되며, 이 조건이 참(‘true’)일 때 반복 블록 내의 코드가 실행됩니다. 조건이 거짓(‘false’)이 되면 반복문은 종료됩니다.
2.2 while 반복문 예시.
간단한 예
int i = 0;
while (i < 5) {
System.out.println("i의 값은: " + i);
i++; // i 값을 증가시켜 조건이 eventually 거짓이 되도록 함
}
이 예제에서는 ‘i’ 가 0부터 시작하여 5미만인 동안 반복문을 실행합니다.
반복문 내에서 ‘i’ 를 1씩 증가시켜 eventually 조건이 거짓이 되도록 합니다.
결과적으로 ‘i’ 의 값은 0부터 4까지 콘솔에 출력됩니다.
사용자 입력 받기
‘while’ 문은 사용자 입력을 받고, 그 입력에 따라 반복을 계속할지 여부를 결정할 때 유용하게 사용될 수 있습니다.
예를 들어, 사용자가 특정 문자를 입력할 때까지 입력을 계속 받는 프로그램은 다음과 같이 작성할 수 있습니다.
Scanner scanner = new Scanner(System.in);
String input = "";
while (!input.equals("종료")) {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요: ");
input = scanner.nextLine();
}
scanner.close();
2.3 주의사항.
‘while’ 반복문을 사용할 때는 반복문 내에서 조건이 eventually(결국) 거짓이 될 수 있도록 조치를 취해야 합니다.
그렇지 않으면, 조건이 항상 참으로 평가될 경우 무한 후프에 빠질 수 있습니다.
따라서 조건 변수를 적절히 조작하거나 적절한 로직을 구현하여 반복문이 적절한 시점에 종료될 수 있도록 해야 합니다.
📝 정리.
‘while’ 문은 그 구조가 간단하고 유연하여, 특정 조건 하에 반복 실행을 해야 할 때 매우 유용한 프로그래밍 도구입니다.
3. do-while 반복문.
자바 프로그래밍에서 ‘do-while’ 반복문은 조건을 검사하기 전에 최소 한 번은 코드 블록을 실행하는 반복문입니다.
이 구조는 ‘while’ 반복문과 비슷하지만, 조건의 참/거짓 여부에 관계없이 최소한 처음에는 반복문 내의 코드를 실행한다는 점이 다릅니다.
‘do-while’ 문은 주로 사용자 입력을 처리하거나, 조건이 반복문의 실행 후에 결정되어야 할 때 유용합니다.
3.1 do-while 반복문의 기본 구조.
‘do-while’ 문의 기본 구조는 다음과 같습니다.
do {
// 최소 한 번은 실행될 코드
} while (조건);
여기서 ‘조건’ 은 반복문의 끝에서 평가됩니다.
조건이 참(‘true’)이면, 코드 블록이 반복적으로 실행됩니다. 조건이 거짓(‘false’)이면, 반복이 종료됩니다.
3.2 do-while 반복문의 예시.
기본 예
int i = 0;
do {
System.out.println("i의 값은: " + i);
i++;
} while (i < 5);
이 예제에서는 ‘i’ 가 0부터 시작하여 ‘i < 5’ 인 동안 반복합니다.
‘do-while’ 문은 ‘i’ 의 초기 값에 상관없이 최소 한 번은 “i의 값은: 0”을 출력하고 시작합니다. 그 후 ‘i’ 가 5미만인 동안 계속해서 반복됩니다.
사용자 입력 받기
사용자로부터 입력을 받고, 특정 조겅(“종료” 문자열 입력)을 만족할 때까지 계속 입력을 받는 프로그램을 구현할 때 ‘do-while’ 문이 유용하게 사용됩니다.
Scanner scanner = new Scanner(System.in);
String input;
do {
System.out.println("문자열을 입력하세요. 종료하려면 '종료'를 입력하세요:");
input = scanner.nextLine();
} while (!input.equals("종료"));
scanner.close();
이 코드는 사용자가 “종료”를 입력할 때까지 계속해서 입력을 받습니다.
입력을 받는 동작은 최소 한 번은 실행되며, 이는 ‘do-while’ 문이 최조 실행을 보장하기 때문입니다.
3.3 주의사항.
‘do-while’ 반복문을 사용할 때, 조건을 적절히 설정하여 반복문이 적절한 시점에 종료되도록 해야 합니다. 그렇지 않으면 무한 루프에 빠질 수 있습니다.
또한, 조건 검사가 반복문의 끝에서 이루어지므로, 조건이 매우 빨리 거짓이 되어도 코드 블록이 한 번은 실행됨을 기억해야 합니다.
📝 정리.
‘do-while’ 문은 조건이 반복 블록 실행 후에만 알 수 있거나, 반복 블록을 적어도 한 번은 실행해야 하는 경우 특히 유용한 도구입니다.
4. continue.
자바 프로그래밍에서 ‘continue’ 문은 반복문 내에서 사용되며, 그것이 실행될 때 현재 반복의 나머지 부분을 건너뛰고 즉시 다음 반복으로 넘어가도록 합니다.
이를 통해 특정 조건에서 반복문의 다음 순환을 즉시 시작할 수 있게 해줍니다.
‘continue’ 는 주로 반복문 내에서 특정 조건에 대한 예외 처리나 불필요한 처리를 건너뛰기 위해 사용됩니다.
4.1 continue 기본 사용법
‘continue’ 문은 주로 ‘for’, ‘while’, 또는 ‘do-while’ 반복문 내에서 사용됩니다.
간단하게는 ‘continue’ 를 실행하면 반복문의 현재 순회에서 남은 코드를 실행하기 않고, 다음 반복으로 진행합니다.
4.2 continue 예시
‘for’ 반복문에서의 사용
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 짝수인 경우, 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 코드는 0부터 9까지의 숫자 중에서 홀수만 출력합니다.
‘i’ 가 짝수일 경우, ‘continue’ 문이 실행되어 ‘System.out.println(i);’ 줄을 건너뛰고 다음 반복으로 넘어갑니다.
‘while’ 반복문에서의 사용
int i = 0;
while (i < 10) {
i++;
if (i % 2 == 0) {
continue; // 짝수인 경우, 아래의 출력을 건너뛰고 다음 반복으로 넘어감
}
System.out.println(i); // 홀수만 출력
}
이 예제에서도 ‘continue’ 는 짝수를 확인하는 조건에서 참일 경우 나머지 코드를 건너뛰고 다음 반복을 계속 진행하도록 합니다.
4.3 continue 특징 및 주의사항.
‘continue’ 문은 현재 수행 중인 반복의 나머지 부분을 건너뛰고, 반복문의 조건 검사로 직접 이동하여 다음 반복을 시작합니다.
‘continue’ 문을 사용할 때는 반복문이 무한 루프에 빠지지 않도록 주의해야 합니다.
예를 들어, ‘continue’ 문이 반복문의 변수 값을 변경하는 코드를 건너뛰면 그 변수의 값이 업데이트되지 않아 무한 루프가 발생할 수 있습니다.
‘continue’ 는 루프의 흐름을 제어하고, 코드의 읽기 어려움을 증가시킬 수 있으므로, 사용할 때는 명확한 이유가 있어야 합니다.
📝 정리.
‘continue’ 문은 코드를 보다 효율적으로 만들고, 필요 없는 조건을 빠르게 건너뛸 수 있게 도와줍니다.
그러나 코드의 가독성과 유지 관리에 영향을 줄 수 있으므로 신중하게 사용해야 합니다.
5. break문.
자바 프로그래밍에서 ‘break’ 문은 반복문(‘for’, ‘while’, ‘do-while’) 또는 ‘switch’ 문에서 현재 블록의 실행을 즉시 종료하고, 해당 블록의 바깥으로 제어를 이동시키는 역할을 합니다.
이는 반복문 또는 ‘switch’ 문 내에서 특정 조건을 만족할 때 추가적인 처리 없이 루프나 선택 구조를 벗어나기 위해 사용됩니다.
5.1 break 기본 사용법.
반복문에서의 사용 : ‘break’ 를 사용하여 무한 루프를 종료하거나 특정 조건이 만족될 때 반복문을 조기에 종료할 수 있습니다.
‘switch’ 문에서의 사용 : 각 ‘case’ 블록 뒤에 ‘break’ 를 사용하여 ‘switch’ 문을 종료하고, 다음 ‘case’ 로 넘어가지 않도록 합니다.
5.2 break 예시
반복문에서의 ‘break’
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // i가 5가 되면 for 루프를 종료
}
System.out.println(i);
}
이 코드에서는 ‘i’ 가 5에 도달하면 ‘break’ 문이 실행되어 ‘for’ 루프가 즉시 종료됩니다.
결과적으로, 0부터 4까지의 숫자만 출력됩니다.
‘while’ 반복문에서의 ‘break’
int i = 0;
while (true) { // 무한 루프
if (i == 5) {
break; // i가 5가 되면 while 루프를 종료
}
System.out.println(i);
i++;
}
이 예제에서도 ‘i’ 가 5일 때 ‘break’ 문을 사용하여 무한 루프를 종료합니다.
‘switch’ 문에서의 ‘break’
int number = 2;
switch (number) {
case 1:
System.out.println("Number is 1");
break;
case 2:
System.out.println("Number is 2");
break;
case 3:
System.out.println("Number is 3");
break;
default:
System.out.println("Number is not 1, 2, or 3");
}
‘switch’ 문에서 ‘number’ 가 2일 때 해당하는 ‘case’ 블록이 실행되고, ‘break’ 문으로 인해 ‘switch’ 문을 벗어나게 됩니다.
5.3 break문 특징 및 주의사항
‘break’ 문은 코드 실행을 즉시 중단시키므로, 효과적인 프로그램 흐름 제어를 가능하게 합니다.
반복문이나 ‘switch’ 문 내에서만 ‘break’ 문을 사용할 수 있습니다.
‘break’ 문을 사용할 때는 코드의 흐름을 명확히 이해하고 있어야 하며, 무분별한 사용은 코드의 가독성과 유지보수를 어렵게 만들 수 있습니다.
📝 정리.
‘break’ 는 코드의 복잡성을 줄이고 특정 조건에서 즉시 반복문을 종료할 수 있는 강력한 도구입니다.
그러나 그 사용은 코드의 구조를 명확하게 유지하는 방식으로 신중하게 이루어져야 합니다.
6. for-each문.
자바 프로그래밍에서 ‘for-each’ 문, 또는 강화된 ‘for’ 문(enhanced for loop)은 배열이나 컬렉션 프레임워크에 저장된 각 요소를 순회하기 위해 사용되는 구문입니다.
기존의 ‘for’ 문보다 간결하며, 코드를 읽고 작성하기가 더 쉬워 배열이나 컬렉션의 모든 요소에 접근할 때 일반적으로 권장되는 방식입니다.
6.1 for-each문 기본 구조.
‘for-each’ 문의 기본 구조는 다음과 같습니다.
for (타입 변수명 : 배열 또는 컬렉션) {
// 변수명을 사용한 코드
}
여기서 ‘타입’ 은 배열 또는 컬렉션에 저장된 요소의 타입을 말하고, ‘변수명’ 은 반복되는 각 요소를 참조하는 데 사용되는 변수 이름입니다.
‘배열 또는 컬렉션’ 은 순회할 배열이나 컬렉션 객체를 지정합니다.
6.2 for-each문 예시
배열 사용 예
int[] numbers = {1,2,3,4,5};
for(int number: numbers) {
System.out.println(number);
}
이 코드에서 ‘for-each’ 문은 ‘numbers’ 배열의 모든 요소를 순회합니다.
각 반복에서 ‘number’ 변수에 배열의 요소가 할당되며, 그 값을 출력합니다.
컬렉션 사용 예
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (String name : names) {
System.out.println(name);
}
이 예에서는 ‘names’ 리스트의 모든 요소를 순회합니다.
각 요소는 ‘name’ 변수에 할당되고, ‘System.out.println’ 을 통해 출력됩니다.
6.3 for-each문의 장점.
가독성 향상: ‘for-each’ 문은 간결하고 이해하기 쉬워, 코드의 가독성을 크게 향상시킵니다.
오류 감소: 전통적인 ‘for’ 문에서 발생할 수 있는 인덱스 관련 실수나 경계 조건 오류를 방지할 수 있습니다.
향상된 추상화: 컬렉션의 내부 구조나 크기를 몰라도 각 요소에 접근할 수 있습니다.
6.4 for-each문의 제한사항.
컬렉션 수정 불가: ‘for-each’ 문을 사용하는 동안 컬렉션을 수정할 수 없습니다.
예를 들어, 순회 중인 컬렉션에서 요소를 추가하거나 제거할 수 없습니다.
인덱스 접근 불가: ‘for-each’ 문은 각 요소에 대한 인덱스를 제공하지 않습니다.
특정 인덱스의 요소에 접근하거나 인덱스를 활용한 복잡한 로직이 필요한 경우에는 전통적인 ‘for’ 문을 사용해야 합니다.
📝 정리.
‘for-each’ 문은 자바에서 컬렉션과 배열을 효율적으로 처리할 수 있는 강력하고 사용하기 쉬운 도구입니다.
이를 통해 코드를 더욱 간결하고 안전하게 만들 수 있습니다.
-
-
💾 [CS] 메모리의 주소 공간
메모리의 주소 공간.
주소에는 물리 주소와 논리 주소가 있다. 이번 절에서는 이 두 주소의 개념과 차이, 그리고 두 주소 간의 변환 방법을 학습한다.
1. 주소의 종류.
지금까지 ‘메모리에 저장된 정보의 위치는 주소로 나타낼 수 있다’ 정도로만 설명했지만, 사실 주소에는 두 종류가 있습니다.
1. 물리주소 : 메모리 하드웨어가 사용하는 주소.
2. 논리주소 : CPU와 실행 중인 프로그램이 사용하는 주소.
2. 물리 주소와 논리 주소.
CPU와 실행 중인 프로그램은 현재 메모리 몇 번지에 무엇이 저장되어 있는지 다 알고 있지 않습니다.
그 이유는 메모리에 저장된 정보는 시시각각 변하기 때문입니다.
메모리에는 새롭게 실행되는 프로그램이 시시때때로 적재되고, 실행이 끝난 프로그램은 삭제됩니다.
게다가, 같은 프로그램을 실행하더라도 실행할 때마다 적재되는 주소가 달라질 수 있습니다.
예를 들어, 1500번지에 적재되었던 프로그램을 다시 실행하면 3000번지, 또 다시 실행하면 2700번지에 적재될 수 있습니다.
그렇다면 CPU와 실행 중인 프로그램이 이해하는 주소는 무엇일까요?
주소에는 메모리가 사용하는 물리 주소가 있고, CPU와 실행 중인 프로그램이 사용하는 논리 주소가 있습니다.
물리 주소(Physical address) : 정보가 실제로 저장된 하드웨어상의 주소를 의미.
논리 주소(logical address) : CPU와 실행 중인 프로그램이 사용하는 주소, 실행 중인 프로그램 각각에게 부여된 0번지부터 시작되는 주소를 의미함.
예를 들어 현재 메모리에 메모장, 게임, 인터넷 브라우저 프로그램이 적재되어 있다고 가정해 보겠습니다.
메모장, 게임, 인터넷 브라우저 프로그램은 현재 다른 프로그램들이 메모리 몇 번지에 저장되어 있는지,
다시 말해 다른 프로그램들의 물리 주소가 무엇인지 굳이 알 필요가 없습니다.
새로운 프로그램이 언제든 적재될 수 있고, 실행되지 않은 프로그램은 언제든 메모리에서 사라질 수 있기 때문입니다.
그래서 메모장, 게임, 인터넷 브라우저는 모두 물리 주소가 아닌 0번지부터 시작하는 자신만을 위한 주소인 논리 주소를 가지고 있습니다.
예를 들어, ‘10번지’라는 주소는 메모장에도, 게임에도, 인터넷 브라우저에도 논리 주소로써 존재할 수 있습니다.
프로그램마다 같은 논리 주소가 얼마든지 있을 수 있다는 뜻입니다.
그리고 CPU는 이 논리 주소를 받아들이고, 해석하고, 연산합니다.
정리하면, 메모리가 사용하는 주소는 하드웨어상의 실제 주소인 물리 주소이고, CPU와 실행 중인 프로그램이 사용하는 주소는 각각의 프로그램에 부여된 논리 주소입니다.
그런데 CPU가 이해하는 주소가 논리 주소라고는 해도 CPU가 메모리와 상호작용하려면 논리 주소와 물리 주소 간의 변환이 이루어져야 합니다.
논리 주소와 물리 주소 간에 어떠한 변환도 이루어지지 않는다면 CPU와 메모리는 서로 이해할 수 없는 주소 체계를 가지고 각자 다른 이야기만 할 뿐 결코 상호작용할 수 없을 테니까요.
그렇다면 논리 주소는 어떻게 물리 주소로 변환될까요?
논리 주소와 물리 주소 간의 변환은 CPU와 주소 버스 사이에 위치한 메모리 관리 장치(MMU: Memory Management Unit) 라는 하드웨어에 의해 수행됩니다.
MMU는 CPU가 발생시킨 논리 주소에 베이스 레지스터 값을 더하여 논리 주소를 물리 주소로 변환합니다.
예를 들어 현재 베이스 레지스터에 15000이 저장되어 있고 CPU가 발생시킨 논리 주소가 100번지라면 이 논리 주소는 아래 그림처럼 물리 주소 15100번지(100+15000)로 변환됩니다.
물리 주소 15000번지부터 적재된 프로그램 A의 논리 주소 100번지에는 이렇게 접근이 가능한 것 입니다.
베이스 레지스터는 프로그램의 가장 작은 물리 주소, 즉 프로그램의 첫 물리 주소를 저장하는 셈이고,
논리 주소는 프로그램의 시작점으로부터 떨어진 거리인 셈입니다.
3. 메모리 보호 기법.
메모장 프로그램의 물리 주소가 1000번지부터 1999번지, 인터넷 브라우저 프로그램의 물리 주소가 2000번지부터 2999번지, 게임 프로그램의 물리 주소가 3000번지부터 3999번지라고 가정해 보겠습니다.
만약 메모장 프로그램 명령어 중 ‘(논리 주소) 1500번지에 숫자 100을 저장하라’와 같은 명령어가 있다면 숫자 100은 어떤 물리 주소에 저장될까요? 이 명령어는 실행되어도 안전할까요?
혹은 인터넷 브라우저 프로그램 명령어 중 ‘(논리 주소) 1100번지의 데이터를 삭제하라’와 같은 명령어가 있다면 어떤 물리 주소의 데이터가 삭제될까요? 이 명령어는 실행되어도 안전할까요?
위와 같은 명령어들은 실행되어서는 안 됩니다.
프로그램의 논리 주소 영역을 벗어났기 때문입니다.
위 명령어들이 실행된다면 메모장 프로그램 명령어는 애꿏은 인터넷 브라우저 프로그램에 숫자 10을 저장하고, 인터넷 브라우저 프로그램 명령어는 자신과는 전혀 관련 없는 게임 프로그램 정보를 삭제합니다.
이렇게 다른 프로그램의 영역을 침범할 수 있는 명령어는 위험하기 때문에 논리 주소 범위를 벗어나는 명령어 실행을 방지하고 실행 중인 프로그램이 다른 프로그램에 영향을 받지 않도록 보호할 방법이 핑요합니다.
이는 한계 레지스터(limit register) 라는 레지스터가 담당합니다.
베이스 레지스터가 실행 중인 프로그램의 가장 작은 물리 주소를 저장한다면, 한계 레지스터는 논리 주소의 최대 크기를 저장합니다.
즉, 프로그램의 물리 주소 범위는 베이스 레지스터 값 이상, 베이스 레지스터 값 + 한계 레지스터 값 미만이 됩니다.
CPU가 접근하려는 논리 주소는 한계 레지스터가 저장한 값보다 커서는 안 됩니다.
한계 레지스터보다 높은 주소 값에 접근하는 것은 곧 프로그램의 범위에 벗어난 메모리 공간에 접근하는 것과 같디 때문입니다.
베이스 레지스터에 100, 한계 레지스터에 150이 저장되어 있다고 해 봅시다.
이는 물리 주소 시작점이 100번지, 프로그램의 크기(논리 주소의 최대 크기)는 150임을 의미합니다.
따라서 이 프로그램은 150번지를 넘어서는 논리 주소를 가질 수 없습니다.
이번에는 베이스 레지스터에 1500, 한계 레지스터에 1000이 저장되어 있다고 해 봅시다.
이는 물리주소 시작점이 1500번지, 프로그램 크기는 1000임을 의미합니다.
따라서 이 프로그램은 1000번지를 넘어서는 논리 주소를 가질 수 없습니다.
CPU는 메모리에 접근하기 전에 접근하고자 하는 논리 주소가 한계 레지스터보다 작은지를 항상 검사합니다.
만약 CPU가 한계 레지스터보다 높은 논리 주소에 접근하려고 하면 인터럽트(트랩)를 발생시켜 실행을 중단합니다.
이러한 방식으로 실행 중인 프로그램의 독립적인 실행 공간을 확보하고 하나의 프로그램이 다른 프로그램을 침범하지 못하게 보호할 수 있습니다.
5. 키워드로 정리하는 핵심 키워드
물리 주소는 메모리 하드웨어상의 주소이고, 논리 주소는 CPU와 실행 중인 프로그램이 사용하는 주소입니다.
MMU는 논리 주소를 물리 주소로 변환합니다.
베이스 레지스터는 프로그램의 첫 물리 주소를 저장합니다.
한계 레지스터는 실행 중인 프로그램의 논리 주소의 최대 크기를 저장합니다.
컴퓨터 시스템에서 “물리 주소(Physical Address)”와 “논리 주소(Logical Address)”는 메모리 관리의 중요한 개념입니다.
각각은 다음과 같은 의미를 가지며, 시스템의 효율적인 메모리 관리를 위해 사용됩니다.
1.1 논리 주소(Logical Address)
정의 : 논리 주소는 프로그램이 사용하는 주소입니다.
이 주소는 프로그램이 실행되면서 생성되는 주소로, 사용자 또는 프로그램이 접근할 수 있는 주소입니다.
이 주소는 가상 메모리 주소라고도 하며, 실제 메모리의 물리적 위치와는 독립적입니다.
목적 : 논리 주소의 주요 목적은 각 프로세스가 독립된 주소 공간을 갖게 하여, 프로세스간의 메모리 충돌을 방지하고 보안을 강화하는 데 있습니다.
또한, 프로그래밍을 단순화시키고 메모리 관리를 더 유연하게 만듭니다.
1.2 물리 주소(Physical Address)
정의 : 물리 주소는 메모리 장치 내의 실제 위치를 가리키는 주소입니다.
이 주소는 시스템의 메모리 관리 유닛(Memory Management Unit, MMU)에 의해 사용되며, 실제 RAM에서 데이터를 찾는 데 사용됩니다.
목적 : 물리 주소는 시스템의 메모리를 효율적으로 할당하고 관리하는 데 필요합니다.
이를 통해 시스템은 실제 메모리 공간을 최적화하고, 필요한 데이터와 프로그램을 정확한 위치에서 처리할 수 있습니다.
1.3 주소 변환(Address Translation)
논리 주소에서 물리 주소로의 변환은 주로 메모리 관리 유닛(MMU)에 의해 수행됩니다.
이 과정은 다음과 같은 방법으로 이루어 집니다.
1. 페이지 테이블 : 운영체제는 페이지 테이블을 사용하여 논리 주소를 물리 주소로 매핑합니다.
페이지 테이블을 논리 주소를 페이지로 나누고, 각 페이지가 실제 메모리의 어느 부분에 해당하는지를 나타내는 테이블입니다.
2. 변환 조회 버퍼(TLB) : 변환 조회 버퍼는 자주 사용되는 주소 매핑을 캐시하는 작은 메모리로, 주소 변환 과정을 빠르게 만듭니다.
3. 주소 변환 과정
프로세스가 논리 주소를 생성합니다.
MMU는 논리 주소의 페이지 번호를 확인하고, 해당 페이지 번호가 페이지 테이블에 있는지 확인합니다.
페이지 테이블에서 해당 페이지의 물리 주소를 찾아 매핑합니다.
물리 주소를 사용하여 실제 메모리에서 데이터를 엑세스합니다.
📝 정리
이러한 주소 변환 메커니즘은 메모리 보호, 프로세스 격리, 메모리 사용의 효율성 증가 등을 가능하게 하며, 복잡한 현대의 멀티태스킹 환경에서 중요한 역할을 합니다.
Q1. 물리 주소(Physical Address)’와 ‘논리 주소(Logical Address)’에 대해 설명해 주시겠습니까? 이 두 주소의 개념과 차이점을 구체적으로 말씀해 주시고, 어떻게 논리 주소가 물리 주소로 변환되는지 그 과정에 대해서도 설명해 주세요.
논리 주소는 프로그램이 사용하는 주소로, 프로그램 코드에 의해 참조되는 주소입니다. 이는 운영체제에 의해 관리되며, 프로그램이 메모리에 로드되는 위치와 무관하게 일관성을 유지합니다. 즉, 프로그램이 메모리의 어느 위치에 로드되든지 간에 같은 논리 주소를 사용할 수 있습니다. 논리 주소는 가상 메모리 주소라고도 하며, 이를 통해 개발자는 실제 메모리 구조를 신경 쓰지 않고 프로그래밍할 수 있습니다.
물리 주소는 메모리 장치 내의 실제 물리적 위치를 가리킵니다. 즉, 물리 주소는 RAM 내의 실제 데이터나 명령어가 저장된 위치를 나타내며, 메모리 관리 유닛(MMU)에 의해 논리 주소로부터 변환됩니다.
논리 주소에서 물리 주소로의 변환은 주로 메모리 관리 유닛(MMU)을 통해 이루어집니다. 이 과정은 다음과 같습니다:
프로세스가 생성하는 논리 주소는 페이지 번호와 오프셋으로 구성됩니다.
페이지 번호는 페이지 테이블을 참조하여 해당 페이지가 메모리의 어느 물리적 위치에 있는지 결정합니다. 이 페이지 테이블은 운영 체제에 의해 관리되며, 각 페이지의 물리 주소를 저장합니다.
물리 주소는 결정된 페이지 시작 주소에 오프셋을 추가하여 최종적으로 결정됩니다.
변환 조회 버퍼(TLB)는 이러한 변환 과정을 가속화하기 위해 자주 사용되는 주소 변환을 캐시합니다.
이러한 변환 과정을 통해 시스템은 효율적으로 메모리를 관리하며, 프로세스 간 메모리 격리와 보안을 유지할 수 있습니다.
-
☕️[Java] 조건문
1️⃣ 조건문
1. if문.
자바 프로그래밍에서 ‘if’ 문은 조건부 실행을 제어하는 기본적인 제어 구문입니다.
이를 통해 프로그램은 주어진 조건이 참(‘true’)인지 거짓(‘false’)인지에 따라 다른 행동을 취할 수 있습니다.
1.1 if문 기본 구조.
‘if’ 문의 기본 구조는 다음과 같습니다.
if (조건) {
// 조건이 참일 때 실행될 코드
}
여기서 ‘조건’ 은 boolean 타입의 표현식으로, 평가 결과가 ‘true’ 또는 ‘false’ 가 됩니다.
조건이 ‘true’ 일 때만 중괄호 ’{}’ 내부의 코드가 실행됩니다.
1.2 예시
예를 들어, 사용자의 나이가 성인 기준을 만족하는지를 확인하는 코드는 다음과 같습니다.
int age = 20;
if (age >= 18) {
System.out.println("성인입니다.");
}
이 코드에서 ‘age >= 18’ 이라는 조건이 참이면 “성인입니다.” 라는 메시지를 출력합니다.
1.3 ‘else’와 ‘else if’ 확장
‘if’ 문은 종종 ‘else’ 와 ‘else if’ 와 함께 사용되어 보다 복잡한 조건 로직을 구현할 수 있습니다.
if (조건1) {
// 조건1이 참일 때 실행될 코드
} else if (조건2) {
// 조건1이 거짓이고 조건2가 참일 때 실행될 코드
} else {
// 위의 모든 조건이 거짓일 때 실행될 코드
}
예를 들어, 점수에 따라 학점을 출력하는 코드는 다음과 같습니다.
int score = 85;
if (score >= 90) {
System.out.println("학점 A");
} else if (score >= 80) {
System.out.println("학점 B");
} else if (score >= 70) {
System.out.println("학점 C");
} else {
System.out.println("학점 D");
}
이 예제에서 ‘score’ 변수의 값에 따른 다른 학점을 출력합니다.
‘if’, ‘else if’, ‘else’ 구문은 점수 범위에 따라 조건적으로 실행되며, 가장 먼저 만족하는 조건의 블록만 실행됩니다.
📝 정리.
‘if’ 문은 프로그래밍에서 결정을 내리는 데 필수적인 구조이며, 다양한 조건에 따라 코드의 실행 흐름을 제어하는 데 사용됩니다.
2. switch문.
자바 프로그래밍에서 ‘switch’ 문은 다수의 조건 중 하나를 선택해 실행할 때 사용하는 조건문입니다.
이는 ‘if-else’ 조건문의 대안으로, 변수의 값에 따라 여러 실행 경로 중 하나를 선택할 수 있도록 해줍니다.
‘switch’ 문은 특히 특정 변수가 취할 수 있는 명확한 값들을 기반으로 다양한 케이스를 처리할 때 유용하게 사용됩니다.
2.1 switch문의 기본 구조.
‘switch’ 문의 기본 구조는 다음과 같습니다.
switch (표현식) {
case 값1:
// 표현식 결과가 값1과 일치할 때 실행할 코드
break;
case 값2:
// 표현식 결과가 값2와 일치할 때 실행할 코드
break;
// 추가적인 case들을 더 정의할 수 있습니다.
default:
// 어떤 case도 일치하지 않을 때 실행할 코드
}
2.2 switch 문의 주요 특징.
1. 표현식 은 주로 정수, 문자형 또는 열거형(enum) 데이터를 사용합니다. 자바 7 이상에서는 문자열(String)도 지원합니다.
2. case 라벨은 ‘switch’ 문 내에서 표현식의 결과와 일치하는 값을 가지며, 해당 값에 대한 실행 코드를 포함합니다.
3. break 문은 ‘switch’ 문을 종료하고 다음 코드로 넘어가도록 합니다. ‘break’ 가 없으면 다음 ‘case’ 로 계속 진행되어 “fall-through” 현상이 발생합니다.
4. default 섹션은 선택적으로 사용되며, 어떤 ‘case’ 도 일치하지 않을 때 실행됩니다.
2.3 switch 문 예시.
학생의 점수에 따라 학점을 부여하는 간단한 예를 들어보겠습니다.
int score = 92;
String grade;
switch (score / 10) {
case 10:
case 9:
grade = "A";
break;
case 8:
grade = "B";
break;
case 7:
grade = "C";
break;
case 6:
grade = "D";
break;
default:
greade = F;
}
System.out.println("학점: " + grade);
이 코드에서 ‘score / 10’ 의 결과값에 따라 다른 ‘case’ 블록이 실행됩니다.
‘92/10’ 은 ‘9’ 이므로, ‘grade’ 는 “A” 가 됩니다.
각 ‘case’ 는 ‘break’ 문으로 종료되므로, 해당 ‘case’ 실행 후, ‘switch’ 문을 벗어납니다.
📝 정리.
‘switch’ 문은 코드의 가독성을 높이고, 많은 조건 분기를 간결하게 처리할 수 있는 방법을 제공합니다.
이는 특히 각 조건이 명확할 때 더욱 유용하며, 코드의 구조를 명확하게 표현할 수 있습니다.
-
☕️[Java] 여러가지 연산자(1)
1️⃣ 각각의 연산자에 대한 이해
1. 항과 연산자.
1.1 단항 연산자(Unary Operator).
자바 프로그래밍에서 단항 연산자(Unary Operator)는 오직 한 개의 피연산자(operand)를 가지고 연산을 수행하는 연산자를 말합니다.
이들은 변수나 값에 직접 적용되며, 표현식의 결과를 반환합니다.
단항 연산자는 특히 간단한 수학 연산, 값의 부정, 또는 값의 증감 등에서 유용하게 사용됩니다.
1.2 자바에서 사용되는 주요 단항 연산자.
1. 부정 연산자('!')
불리언 값을 반전시킵니다.
예를 들어, '!true' 는 'false' 가 됩니다.
2. 단항 플러스 및 마이너스 연산자('+', '-')
'+' 는 일반적으로 숫자의 부호를 그대로 두지만, 명시적으로 사용할 수 있습니다.
'-' 는 숫자의 부호를 반전시킵니다.
예를 들어, '-5' 는 양수 '5' 를 음수로 변환합니다.
3. 증가 및 감소 연산자('++', '--')
'++' 연산자는 변수의 값을 1만큼 증가시킵니다.
'--' 연산자는 변수의 값을 1만틈 감소시킵니다.
이 연산자들은 전위(prefix) 형태(예: '++x')와 후위(postfix) 형태(예: 'x++')로 사용될 수 있습니다.
전위 형태는 변수를 증가시키고 표현식의 값을 반환하기 전에 증가된 값을 사용하고, 후위 형태는 표현식의 값을 반환한 후 변수를 증가시킵니다.
4. 비트 반전 연산자('~')
정수형 변수는 모든 비트를 반전시킵니다.
예를 들어, '~00000000' 은 '11111111' 이 됩니다.
이는 정수에 대해 비트 단위 NOT 연산을 수행합니다.
1.3 예제 사용.
public class UnaryDemo {
public static void main(String[] args) {
boolean a = false;
System.out.println(!a); // true
int num = 5;
System.out.println(-num); // -5
System.out.println(++num); // 6
System.out.println(num++); // 6, 그리고 num이 7이 됨
System.out.println(--num); // 6
System.out.println(num--); // 6, 그리고 num이 5가 됨
int b = 0b00000000; // 이진수로 0
System.out.println(~b); // 모든 비트가 1로 반전
}
}
이 예제에서는 다양한 단항 연산자들의 사용 방법과 그 효과를 보여줍니다.
단항 연산자들은 자바 프로그래밍에서 변수를 조작하거나 특정 연산을 더 간결하게 수행하는데 매우 유용합니다.
2.1 이항 연산자(Binary Operator)
자바 프로그래밍에서 이항 연산자(Binary Operator)는 두 개의 피연산자(operand)를 취해 연산을 수행하고 결과를 반환하는 연산자를 말합니다.
이항 연산자는 수학적 계산, 논리 비교, 값의 할당 등 다양한 작업에 사용됩니다.
2.2 자바에서 사용되는 주요 이항 연산자의 종류.
1. 산술 연산자.
’+’ (덧셈)
’-‘ (뺄셈)
‘*‘ (곱셈)
’/’ (나눗셈)
’%’ (나머지)
2. 비교 연산자.
’==’ (동등)
’!=’ (부등)
’>’ (크다)
’<’ (작다)
’>=’ (크거나 같다)
’<=’ (작거나 같다)
3. 논리 연산자.
‘&&’ (논리적 AND)
**’
‘** (논리적 OR)
4. 비트 연산자
‘&’ (비트 AND)
**’
‘** (비트 OR)
’^’ (비트 XOR)
5. 할당 연산자
’=’ (기본 할당)
’+=’, ‘-=’, ‘*=’, ‘/=’, ‘%=’ (복합 할당 연산자)
**‘&=’, ‘
=’, ‘^=’, ‘«=’, ‘»=’, ‘»>=’** (비트 복합 할당 연산자)
6. 시프트 연산자
’«‘ (왼쪽 시프트)
’»‘ (오른쪽 시프트, 부호 유지)
’»>’ (오른쪽 시프트, 부호 비트 없음)
2.3 예제 코드
public class BinaryOperatorsExample {
public static void main(String[] args) {
int a = 10, b = 5;
int sum = a + b; // 15
int difference = a - b; // 5
boolean isEqual = (a == b); // false
boolean isGreater = (a > b); // true
int bitAnd = a & b; // 비트 AND 연산
int shiftedLeft = a << 2; // 왼쪽으로 2 비트 시프트
System.out.println("Sum: " + sum);
System.out.println("Difference: " + difference);
System.out.println("Is Equal: " + isEqual);
System.out.println("Is Greater: " + isGreater);
System.out.println("Bitwise AND: " + bitAnd);
System.out.println("Left Shifted: " + shiftedLeft);
}
}
이항 연산자들은 기본적인 산술 연산부터 복잡한 논리 연산에 이르기까지 프로그래밍에서 광범위하게 사용됩니다.
이를 통해 효과적으로 데이터를 조작하고, 조건을 평가하며, 복잡한 문제를 해결할 수 있습니다.
3.1 삼항 연산자(Ternary Operator).
자바 프로그래밍에서 삼항 연산자(Ternary Operator), 또는 조건 연산자(Conditional Operator)라고 불리는 ’?:’ 는 세 개의 피연산자를 사용하는 유일한 연산자입니다.
이 연산자는 간결한 조건문을 구현할 때 사용되며, 간단한 조건식을 기반으로 두 가지 선택지 중 하나를 반환합니다.
3.2 삼항 연산자의 구조.
조건식 ? 값1 : 값2
1. 조건식 : 이 부분은 ‘true’ 또는 ‘false’ 를 반환하는 불리언 식입니다.
2. 값2 : 조건식이 ‘true’ 일 때 반환됩니다.
3. 값3 : 조건식이 ‘false’ 일 때 반환됩니다.
조건식의 평가 결과에 따라 ‘값1’ 또는 값2 중 하나가 결과값으로 선택되어 반환됩니다.
이 연산자는 일반적으로 간단한 조건에 따라 변수에 값을 할당하거나 특정 표현식의 결과를 결정할 때 유용하게 사용됩니다.
3.3 예제 사용.
public class TernaryExample {
public static void main(String[] args) {
int a = 10, b = 5;
int max = (a > b) ? a : b; // a와 b 중 큰 값을 max에 할당.
System.out.println("Max value: " + max);
String response = (a > b) ? "a is greater than b" : " b is greater or equal to a";
System.out.println(response);
}
}
이 예제에서 삼항 연산자를 사용하여 두 숫자 중 더 큰 숫자를 결정하고, 문자열 메시지도 조건에 따라 선택합니다.
이러한 사용은 코드를 더 간결하게 만들고, ‘if-else’ 구조를 보다 간단하게 대체할 수 있게 해 줍니다.
삼항 연산자는 그 효율성과 간결함 때문에 자바 프로그래밍에서 자주 사용되는 유용한 도구입니다.
그러나 복잡한 로직이나 여러 조건이 연속적으로 필요한 경우에는 가독성을 위해 전통적인 ‘if-else’ 문을 사용하는 것이 더 나을 수 있습니다.
-
☕️[Java] 여러가지 연산자(2)
1️⃣ 비트 연산자에 대한 이해
1. 2진법.
자바 프로그래밍에서의 이진법은 컴퓨터의 지본 숫자 시스템을 참조하는 것입니다.
컴퓨터는 데이터를 0과 1의 형태, 즉 이진수로 처리합니다.
자바에서도 이러한 이진법을 사용하여 데이터를 저장, 처리하며 다양한 연산을 수행할 수 있습니다.
1.1 자바에서 2진법을 사용하는 예.
1. 이진 리터럴 : 자바 7 이상부터는 정수를 이진 리터럴로 직접 표현할 수 있습니다.
예를 들어, ‘int x = 0b1010;’ 은 이진수 ‘1010’ 이고, 십진수로 10입니다.
2. 비트 연산자 : 자바는 비트 연산을 수행할 수 있는 여러 연산자를 제공합니다.
예를 들어, 다음과 같습니다.
'&' (AND 연산자)
'|' (OR 연산자)
'^' (XOR 연산자)
'~' (NOT 연산자)
'<<' (왼쪽 시프트)
'>>' (오른쪽 시프트)
'>>>' (부호 없는 오른쪽 시프트)
이들 연산자는 주로 효율적인 수치 계산, 저수준 프로그래밍, 암호와 작업 등에 사용됩니다.
3. 이진 데이터 조작 : 파일이나 네트워크를 통해 바이트 단위로 데이터를 읽고 쓸 때, 이진 형식으로 데이터를 처리합니다.
자바에서는 'byte' 자료형을 이용하여 이진 데이터를 직접 다룰 수 있습니다.
📝 정리
이진법을 사용하는 주된 이유는 컴퓨터 하드웨어가 전기 신호로 작동하기 때문에 0과 1, 즉 이진 상태를 나타내는 전기의 켜짐과 꺼짐 상태로 모든 데이터를 표현하기 편리하기 때문입니다.
이렇게 함으로써, 프로그래밍에서 더욱 직접적이고 효율적인 하드웨어 조작이 가능해집니다.
2. 2의 보수.
자바 프로그래밍에서의 2의 보수(2’s complement)는 음수를 표현하기 위한 방법입니다.
컴퓨터 시스템은 보통 이진법을 사용하여 데이터를 저장하고 처리하는데, 이진법에서 음수를 표현하기 위해 가장 널리 사용되는 방법이 2의 보수입니다.
2.1 2의 보수 생성 과정.
1. 원래 숫자의 이진 표현을 얻습니다.
예를 들어, 5의 이진 표현은 ‘0101’ 입니다.
2. 이진 표현의 모든 비트를 반전시킵니다.
즉, 0은 1로, 1은 0으로 변경합니다.
5의 경우 ‘0101’ 이 ‘1010’ 이 됩니다.
3. 반전된 값에 1을 더합니다.
이렇게 하면 ‘1011’ 이됩니다.
이렇게 생성된 ‘1011’ 은 -5를 나타냅니다.
이 방법은 자바를 포함한 대부분의 프로그래밍 언어와 컴퓨터 시스템에서 음수를 표현하는 표준 방법입니다.
2.2 2의 보수의 장점.
덧셈 연산만으로 뺄셈을 할 수 있습니다.
예를 들어, 5-5를 계산하려면 5와 -5의 2의 보수를 더하면 됩니다.
이진법으로는 ‘1010 + 1011 = 10000’ 이고, 최상위 비트(캐리 비트)는 무시합니다.
따라서 결과는 ‘0000’ 이 됩니다.
오버플로 처리가 간단합니다.
캐리 비트는 무시하면서 자연스럽게 오버플로를 처리할 수 있습니다.
2.3 자바에서의 활용.
자바에서는 정수형 타입(‘int’, ‘long’, ‘short’, ‘byte’) 이 이진법으로 2의 보수 형태로 저장되고 처리됩니다.
이는 자바의 모든 정수 연산에 내장된 메커니즘입니다.
예를 들어, 자바에서 ‘-5’ 를 선언하면 내부적으로는 ‘5’ 의 2의 보수인 ‘111…11011’ (32비트 시스템에서의 표현)으로 저장됩니다.
📝 정리
2의 보수 방식은 음수를 다루기 위한 효과적인 방법이며, 프로그래머가 별도의 조치를 취하지 않아도 시스템이 자동으로 처리해 주기 때문에 매우 편리합니다.
3. 비트 논리연산자.
자바 프로그래밍에서 비트 논리연산자는 비트 단위로 논리 연산을 수행하는 연산자입니다.
이들 연산자는 주로 정수 타입의 변수에 사용되며, 각 비트를 독립적으로 비교하여 결과를 반환합니다.
비트 논리연산자는 주로 저수준 프로그래밍, 효율적인 데이터 처리, 상태 플래그 관리, 암호화 등의 작업에 활용됩니다.
자바에서 사용되는 주요 비트 논리 연산자는 다음과 같습니다.
1. AND 연산자(‘&’) : 두 피연산자의 비트가 모두 1일 경우에만 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 & 3’ 은 이진수로 ‘0101 & 0011’ 을 계산하여 ‘0001’ 이 되므로, 결과는 ‘1’ 입니다.
**2. OR 연산자(‘
’) :** 두 피연산자 중 하나라도 비트가 1이면 결과의 해당 비트를 1로 설정합니다.
예를 들어, **‘5
3’** 은 이진수로 **‘0101
0011’** 을 계산하여 ‘0111’ 이 되므로, 결과는 ‘7’ 입니다.
3. XOR 연산자(‘^’) : 두 피연산자의 비트가 서로 다를 경우 결과의 해당 비트를 1로 설정합니다.
예를 들어, ‘5 ^ 3’ 은 이진수로 ‘0101 ^ 0011’ 을 계산하여 ‘0110’ 이 되므로, 결과는 ‘6’ 입니다.
4. NOT 연산자(‘~’) : 피연산자의 모든 비트를 반전시킵니다.(1은 0으로, 0은 1로).
예를 들어, ‘~5’ 는 이진수로 ‘~0101’ 을 계산하여 ‘…1010’(무한히 많은 1 다음에 1010)이 되고, 이는 보통 32비트 시스템에서 ‘-6’ 으로 해석됩니다.
5. 왼쪽 시프트(‘«’) : 모든 비트를 왼쪽으로 지정된 수만틈 이동시키고, 오른쪽은 0으로 채웁니다.
예를 들어.‘3 « 2’ 는 ‘0011’ 을 왼쪽으로 2비트 이동하여 ‘1100’ 이 되므로, 결과는 ‘12’ 입니다.
6. 오른쪽 시프트(‘»’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 최상위 비트(부호 비트)의 값으로 채웁니다.
예를 들어, ‘-8 » 2’ 는 ‘11111000’ 을 오른쪽으로 2비트 이동하여 ‘11111110’ 이 되므로, 결과는 ‘-2’ 입니다.
7. 부호 없는 오른쪽 시프트(‘»>’) : 모든 비트를 오른쪽으로 지정된 수만큼 이동시키고, 왼쪽은 0으로 채웁니다. 이는 부호 비트를 무시하고, 순수하게 비트를 오른쪽으로 이동시키기 때문에 음수에 사용했을 때 결과가 달라집니다.
📝 정리
이러한 비트 논리 연산자들은 데이터의 특정 비트를 직접 조작할 필요가 있는 경우에 유용하며, 자바 프로그래밍에서 중요한 도구입니다.
-
-
☕️[Java] 변수와 자료형(4)
변수와 자료형(4)
1️⃣ 자료형에 대한 이해
1. List
자바 프로그래밍에서 List 는 일련의 요소를 저장하는 데 사용되는 순차적인 컬렉션을 나타냅니다.
이는 자바의 java.util.List 인터페이스를 통해 제공되며, 이는 주문된 컬렉션을 관리하기 위한 다양한 메소드를 제공합니다.
List 는 중복된 요소를 포함할 수 있고, 각 요소는 리스트 내에서 특정 위치를 가집니다.
사용자는 이 위치를 인덱스로 사용하여 리스트의 요소에 접근할 수 있습니다.
List 인터페이스의 주요 특징은 다음과 같습니다.
1. 순서 보장 : 리스트는 요소들이 추가된 순서를 유지하며, 각 요소는 특정 인덱스를 통해 접근할 수 있습니다.
2. 요소의 중복 허용 : 같은 값을 가진 요소를 여러 개 포함할 수 있습니다.
3. 동적 배열 : 리스트의 크기는 고정되어 있지 않고, 요소를 추가하거나 삭제함에 따라 동적으로 조절됩니다.
자바에서는 List 인터페이스를 구현하는 몇 가지 클래스가 있습니다.
가장 흔히 사용되는 구현체는 다음과 같습니다.
'ArrayList' : 내부적으로 배열을 사용하여 요소를 저장합니다.
요소의 추가와 인덱스를 통한 접근이 매우 빠르지만, 크기 조절이 필요할 때는 비용이 많이 들 수 있습니다.
'LinkedList' : 각 요소가 다음 요소에 대한 참조와 함께 저장되는 연결 리스트를 사용합니다.
요소의 추가와 삭제는 빠르지만, 인덱스를 통한 요소 접근은 시작부터 요소를 찾을 때까지 순차적으로 검색해야 하므로 시간이 더 걸립니다.
'Vector' : 'ArrayList' 와 비슷하지만, 다중 스레드 환경에서 안전하게 사용할 수 있도록 동기화된 메소드를 제공합니다.
📝 정리.
'List' 는 자바 컬렉션 프레임워크의 일부이며, 데이터를 관리하고 처리하는 데 매우 유용합니다.
프로그래머는 이러한 컬렉션을 사용하여 데이터를 유연하게 조작할 수 있습니다.
1.2 List의 주요 메서드.
자바의 'List' 인터페이스에는 여러 가지 중요한 메서드들이 포함되어 있으며, 이를 통해 리스트 내의 요소들을 조작하고 접근할 수 있습니다.
다음은 'List' 인터체이스에서 제공하는 몇 가지 주요 메서드들입니다.
'add(E e)': 리스트의 끝에 요소를 추가합니다.
'add(int index, E element)': 지정된 위치에 요소를 삽입합니다.
'addAll(Collection<? extends E> c)': 지정된 컬렉션의 모든 요소를 리스트의 끝에 추가합니다.
'addAll(int index, Collection<? extends E> c)': 지정된 위치부터 컬렉션의 모든 요소를 리스트에 추가합니다.
'clear()': 리스트에서 모든 요소를 제거합니다.
'contains(Object o)': 리스트가 특정 요소를 포함하고 있는지 확인합니다.
'get(int index)': 지정된 위치의 요소를 반환합니다.
'indexOf(Object o)': 주어진 요소의 첫 번째 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'lastIndexOf(Object o)': 주어진 요소의 마지막 인덱스를 반환합니다. 요소가 리스트에 없는 경우 -1을 반환합니다.
'isEmpty()': 리스트가 비어 있는지 확인합니다.
'iterator()': 리스트의 요소에 대한 반복자를 반환합니다.
'listIterator()': 리스트의 요소를 리스트 순서대로 반복하는 리스트 반복자를 반환합니다.
'remove(Object o)': 주어진 요소를 리스트에서 처음 발견되는 위치에서 제거하고, 그 결과를 반환합니다.
'remove(int index)': 지정된 위치에 있는 요소를 리스트에서 제거하고, 그 요소를 반환합니다.
'replaceAll(UnaryOperator<E> operator)': 주어진 연산자를 사용하여 리스트의 모든 요소를 대체합니다.
'size()': 리스트에 있는 요소의 수를 반환합니다.
'sort(Comparator<? super E> c)': 주어진 비교자를 사용하여 리스트를 정렬합니다.
'subList(int fromIndex, int toIndex)': 지정된 범위의 부분 리스트를 반환합니다.
'toArray()': 리스트 요소를 배열로 반환합니다.
📝 정리.
이 메서드들을 통해 리스트를 생성, 조회, 수정 및 관리하는 다양한 작업을 수행할 수 있습니다.
List 인터페이스를 사용함으로써 데이터를 효율적으로 처리하고 구조화할 수 있습니다.
2. Map
자바 프로그래밍에서 'Map' 은 키(key)와 값(value)의 쌍을 저장하는 객체입니다.
이는 키를 기반으로 빠르게 값을 검색할 수 있게 해주는 데이터 구조로, 각 키는 고유해야 합니다.(즉, 중복된 키를 가질 수 없습니다.)
'Map' 은 'java.util.Map' 인터페이스를 통해 정의되며, ‘HashMap‘, ‘TreeMap‘, ‘LinkedHashMap‘ 등 다양한 구현체를 가집니다.
자바의 ‘Map‘ 인터페이스는 키-값 쌍으로 데이터를 저장하고 관리하는 데 중점을 두는 데이터 구조로서, 특히 다음과 같은 주요 특징을 가지고 있습니다.
1. 키에 의한 값 접근 : ‘Map‘ 은 각 값에 고유한 키를 할당하며, 이 키를 사용하여 빠르게 해당 값을 검색할 수 있습니다.
이는 데이터베이스의 인덱스와 유사한 방식으로 작동합니다.
2. 키의 유일성 : 맵 내에서 모든 키는 고유해야 합니다.
즉, 같은 키가 두 번 이상 존재할 수 없으며, 새로운 키-값 쌍을 추가할 때 이미 존재하는 키를 사용하면 기존의 값이 새 값으로 대체됩니다.
3. 값의 중복 허용 : 키는 유일해야 하지만 값은 중복될 수 있습니다.
다른 키가 동일한 값을 가리킬 수 있습니다.
4. 순서의 유무 : 일반적인 ‘Map‘ 구현체들은 키-값 쌍의 순서를 보장하지 않습니다.
그러나 ‘LinkedHashMap‘ 과 같은 일부 구현체는 요소가 추가된 순서대로 반복할 수 있는 기능을 제공합니다.
‘TreeMap‘ 은 키에 따라 정렬된 순서를 유지합니다.
5. 비동기화 및 동기화 : 기본적으로 대부분의 ‘Map‘ 구현체는 동기화되지 않습니다.(‘HashMap‘). 이는 멀티 스레드 환경에서 동시 수정이 발생할 경우 안전하지 않을 수 있음을 의미합니다.
반면에 ‘Hashtable‘ 과 같은 구현체는 기본적으로 동기화가 되어 있어 멀티 스레드 환경에서 안전합니다.
또한, ‘Collections.synchronizeMap‘ 메소드를 사용하여 맵을 동기화된 맵으로 변환할 수 있습니다.
6. Null 허용 : 대부분의 ‘Map‘ 구현체는 키와 값으로 ‘null‘ 을 허용합니다.(‘HashMap‘, ‘LinkeHashMap‘).
하지만 ‘Hashtable‘ 은 ‘null‘ 키나 값을 허용하지 않으며, ‘TreeMap‘ 은 자연 정렬 또는 ‘Comparator‘ 가 ‘null‘ 을 처리할 수 있는 경우에만 ‘null‘ 키를 허용합니다.
📝 정리.
이러한 특징들로 인해 ‘Map‘ 은 다양한 애플리케이션에서 유연하고 효율적인 데이터 관리를 가능하게 합니다.
데이터를 쉽게 추가, 검색, 삭제할 수 있어 데이터 관리의 복잡성을 줄이고 성능을 최적화하는 데 기여합니다.
3. Generics.
자바 프로그래밍에서 제네릭스(Generics)는 클래스나 메소드에서 사용될 데이터 타입을 추상화하여 코드 작성 시점에는 구체적인 타입을 명시하지 않고, 객체 생성이나 메소드 호출 시점에 실제 사용할 타입을 지정할 수 있도록 하는 프로그래밍 기법입니다.
제네릭스(Generics)는 코드의 재사용성을 높이고, 타입 안정성을 강화하며, 캐스팅에 대한 오류 가능성을 줄이는 데 도움을 줍니다.
3.1 제네릭스(Generics)의 주요 특징.
1. 타입 안전성(Type Safety) : 제네릭스를 사용하면 컴파일 시점에 타입 체크가 가능하여, 실행 시점에서 발생할 수 있는 'ClassCastException' 과 같은 오류를 사전에 방지할 수 있습니다.
2. 재사용성(Reusability) : 하나의 코드를 다양한 타입에 대해 재사용할 수 있습니다.
예를 들어, 제네릭 클래스나 메소드를 정의하면, 다양한 타입의 객체를 저장하거나 처리하는 로직을 단 한번만 작성하여 여러 타입에 걸쳐 사용할 수 있습니다.
3. 코드 간결성(Code Clarity) : 캐스팅을 줄여 코드가 더욱 간결하고 읽기 쉬워집니다.
3.2 제네릭스의 기본 문법.
클래스 선언 : 클래스 이름 뒤에 '<T>' 를 추가하여 제네릭 클래스를 선언합니다.
‘T’ 는 타입 파라미터를 나타내며, 이는 클래스 내에서 사용될 데이터 타입을 대체하는 플레이스홀더 역할을 합니다.
public class Box<T> {
private T t; // T 타입의 객체를 위한 변수
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
메소드 선언 : 메소드 반환 타입 앞에 ’<T>‘ 를 추가하여 제네릭 메소드를 선언합니다.
public <T> T genericMethod(T t) {
return t;
}
제네릭 타입 제한(Bounded Type Parameters) : 특정 클래스의 하위 클래스만 타입 파라미터로 받도록 제한할 수 있습니다.
이는 'extends' 키워드를 사용하여 지정합니다.
public class Box<T extends Number> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
📝 정리.
제네릭스(Generics)를 사용함으로써 개발자는 보다 타입-안전하고 유지보수가 용이한 코드를 작성할 수 있으며, 실행 시 타입 관련 문제를 효과적으로 줄일 수 있습니다.
-
-
☕️[Java] 변수와 자료형(3)
변수와 자료형(3).
1️⃣ 자료형에 대한 이해.
1. String.
String 클래스는 불변(immutable)의 문자열을 다룹니다.
이는 한 번 생성된 String 객체의 내용이 변경될 수 없다는 것을 의미합니다.
문자열을 변경하려고 할 때마다 실제로 새로운 String 객체가 생성되고, 기존 객체는 변경되지 않습니다.
1.1 String의 주요 메소드.
charAt(int index) : 지정된 위치의 문자를 반환합니다.
concat(String str) : 현재 문자열의 끝에 지정된 문자열을 붙여 새로운 문자열을 반환합니다.
contains(CharSequence s) : 특정 문자열이 포함되어 있는지 확인합니다.
startsWith(String prefix) : 문자열이 특정 문자열로 시작하는지 확인합니다.
endsWith(String suffix) : 문자열이 특정 문자열로 끝나는지 확인합니다.
equals(Object anObject) : 문자열이 주어진 객체와 동일한지 비교합니다.
indexOf(int ch), indexOf(String str) : 주어진 문자 또는 문자열의 위치를 찾습니다.
length() : 문자열의 길이를 반환합니다.
replace(char oldChar, char newChar) : 문자열 중 일부 문자를 다른 문자로 대체합니다.
substring(int beginIndex, int endIndex) : 문자열의 부분을 추출합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열의 앞뒤 공백을 제거합니다.
2. StringBuffer.
StringBuffer 클래스는 가변(mutable)의 문자열을 다루며, 문자열 변경 작업이 빈번할 때 사용하면 효율적입니다.
StringBuffer 객체는 내용을 직접 변경할 수 있어, 새로운 객체를 계속 생성하지 않아도 됩니다.
2.1 StringBuffer의 주요 메소드.
append(String str): 문자열의 끝에 주어진 문자열을 추가합니다.
delete(int start, int end): 문자열의 시작 인덱스부터 종료 인덱스 전까지의 부분을 삭제합니다.
deleteCharAt(int index): 지정된 위치의 문자를 삭제합니다.
insert(int offset, String str): 지정된 위치에 문자열을 삽입합니다.
replace(int start, int end, String str): 시작 인덱스부터 종료 인덱스 전까지의 문자열을 새로운 문자열로 대체합니다.
reverse(): 문자열의 순서를 뒤집습니다.
length(): 문자열의 길이를 반환합니다.
capacity(): 현재 버퍼의 크기를 반환합니다.
setCharAt(int index, char ch): 지정된 위치의 문자를 다른 문자로 설정합니다.
📝 정리.
StringBuffer 는 스레드에 안전(thread-safe)합니다, 즉 멀티스레드 환경에서 동시에 접근해도 안전하게 사용할 수 있습니다.
이는 내부적으로 메소드들이 동기화되어 있기 때문입니다.
반면, StringBuilder 는 StringBuffer 와 유사하지만 멀티스레드 환경에서의 동기화 자원이 없어 단일 스레드에서 더 빠르게 작동합니다.
이처럼 String 과 StringBuffer 는 각각의 특성에 맞게 선택하여 사용할 수 있으며, 성능과 사용상황에 따라 적절히 활용하면 됩니다.
3. Array.
자바 프로그래밍에서 배열(Array)은 동일한 타입의 여러 데이터를 연속적인 메모리 위치에 저장하기 위한 자료구조입니다.
배열은 고정된 크기를 가지며, 배열의 각 요소는 같은 데이터 타입을 가집니다.
배열을 사용하면 여러 데이터를 하나의 변수 이름으로 관리할 수 있어 코드를 간결하게 작성할 수 있습니다.
3.1 배열의 특징.
고정된 크기 : 배열은 생성 시 지정된 크기를 변경할 수 없습니다.
배열의 크기는 프로그램 실행 도중에 변경할 수 없으며, 더 많은 데이터를 저장해야 할 경우 새로운 배열을 생성하고 데이터를 복사해야 합니다.
인덱스 접근 : 배열의 각 요소는 인덱스를 통해 접근할 수 있습니다.
인덱스는 0부터 시작하여 배열의 크기 -1까지 번호가 할당됩니다.
동일 타입 : 모든 배열 요소는 동일한 데이터 타입을 가져야 합니다.
예를 들어, int 타입의 배열은 int 타입의 값만을 요소로 가질 수 있습니다.
3.2 배열의 선언과 초기화.
배열을 선언하고 사용하기 위해서는 다음 단계를 따라야 합니다.
1. 배열 선언 : 데이터 타입 뒤에 대괄호 [] 를 사용하여 배열을 선언합니다.
int[] myArray;
String[] stringArray;
2. 배열 생성 : new 키워드를 사용하여 배열을 생성하고, 배열의 크기를 지정합니다.
myArray = new int[10]; // 10개의 정수를 저장할 수 있는 배열
stringArray = new String[5]; // 5개의 문자열을 저장할 수 있는 배열
3. 배열 초기화 : 배열의 각 요소에 값을 할당합니다.
인덱스를 사용하여 접근할 수 있습니다.
myArray[0] = 50;
myArray[1] = 100;
stringArray[0] = "Hello";
stringArray[1] = "World";
3.3 배열 사용 예.
다음은 자바에서 int 배열을 선언, 생성, 초기화하는 예제 코드입니다.
public class ArrayExample {
public static void main(Stringp[] args) {
// 배열 선언과 동시에 생성
int[] numbers = new int[3];
// 배열 초기화
numbers[0] = 7;
numbers[1] = 20;
numbers[2] = 33;
// 배열 사용
System.out.println("첫 번째 숫자: " + numbers[0]);
System.out.println("두 번째 숫자: " + numbers[1]);
System.out.println("세 번째 숫자: " + numbers[2]);
}
}
📝 정리
이 예제에서는 numbers 라는 이름의 int 배열을 생성하고, 세 개의 정수를 저장한 후 출력합니다.
배열을 사용하는 이점 중 하나는 이처럼 단일한 이름으로 여러 데이터를 효율적으로 관리할 수 있다는 것입니다.
-
☕️[Java] 자바 - 변수와 자료형(2)
☕️ 자바 - 변수와 자료형(2)
1. 자료형에 대한 이해
자바 프로그래밍에서 사용되는 자료형은 크게 기본형(Primitive types) 과 참조형(Reference types) 두 가지로 나눌 수 있습니다.
각각의 자료형에 대해 설명드리겠습니다.
1️⃣ 기본형(Primitive types)
기본형 자료형은 실제 값을 저장하는 타입으로, 총 8가지가 있습니다.
정수형
byte : 8비트 정수형, 값의 범위는 -128에서 127까지.
short : 16비트 정수형, 값의 범위는 -32,768에서 32,767까지.
int : 32비트 정수형, 값의 범위는 약 -2.14억에서 2.14억까지.
long : 64비트 정수형, 값의 범위는 약 -9.22경에서 9.22경까지
실수형
float : 32비트 부동 소수점 형. 부정확할 수 있으며, 대략 6~7 자리의 정밀도를 가짐.
double : 64비트 부동 소수점 형. float보다 더 정밀하며, 대략 15자리의 정밀도를 가짐.
문자형
char : 단일 16비트 유니코드 문자를 저장.
논리형
boolean : true 또는 false 값만을 가짐.
2️⃣ 참조형(Reference types)
참조형 자료형은 객체의 참조(메모리 주소)를 저장합니다.
기본형과 달리 메모리의 특정 위치를 가리키는 포인터를 저장하므로, 객체의 크기에 관계없이 참조 변수 크기는 항상 일정합니다.
참조형의 예를 들면 다음과 같습니다.
클래스(Class)
예: String, Integer, File 등
인터페이스(Interface)
예: List, Map, Serializable 등
배열(Array)
예: int[], double[], String[] 등
1.1 인터페이스(Interface)?
자바에서 인터페이스(Interface)는 특정 클래스가 구현해야 할 메소드를 정의하는 “계약”의 역할을 합니다.
이는 클래스가 인터페이스에 정의된 모든 메소드를 반드시 구현하도록 강제합니다.
인터페이스는 메소드의 실제 구현을 포함하지 않고, 메소드의 시그니처(이름, 매개변수 리스트, 반환 유형)만을 정의합니다.
인터페이스를 사용하는 주된 목적은 다음과 같습니다.
1. 추상화(Abstraction) : 인터페이스를 통해 구현의 세부 사항을 숨기고, 사용자에게 필요한 기능만을 제공할 수 있습니다.
이렇게 함으로써 코드의 복잡성을 줄이고, 유지 관리가 쉬워집니다.
2. 다형성(Polymorphism) : 다양한 클래스들이 동일한 인터페이스를 구현함으로써, 다양한 타입의 객체를 동일한 방식으로 처리할 수 있습니다.
이는 코드의 유연성과 재사용성을 높입니다.
3. 결합도 감소(Decoupling) : 인터페이스를 통해 서로 다른 코드 부분 간의 결합도를 낮추어, 각 부분을 독립적으로 개발하고 테스트할 수 있게 합니다.
👉 인터페이스 예시
예를 들어, List 인터페이스는 add ,remove, get, size 등의 메소드를 정의하며, 이 인터페이스를 구현하는 ArrayList, LinkedList 등의 클래스는 이 메소드들을 실제로 구현해야 합니다.
이를 통해 사용자는 구체적인 리스트의 구현 방법을 몰라도 이 인터페이스를 통해 리스트를 사용할 수 있습니다.
이런 방식으로 인터페이스는 참조형 자료형 중 하나로서, 객체의 행동을 정의하고 다양한 구현을 가능하게 합니다.
-
-
-
☕️[Java] 자바 - 변수와 자료형(1)
변수와 자료형(1)
🙋♂️ 변수 이름 규칙
자바 프로그래밍에서 변수를 명명할 때 따라야 할 몇 가지 기본적인 규칙과 관례가 있습니다.
이러한 규칙을 준수하는 것은 코드의 가독성과 유지보수성을 높이는 데 중요합니다.
다음은 자바에서 변수 이름을 지정할 때 고려해야 할 주요 규칙들 입니다.
1️⃣ 기본 규칙.
**문자와 숫자, (Underscore), $ 사용가능 :** 변수 이름은 문자(letter) 나 밑줄(, Underscore) 또는 $(달러 기호)로 시작할 수 있습니다.
그러나 숫자로 시작할 수는 없습니다.
숫자로 시작할 수 없다 : 첫 글자로는 숫자로 변수 이름을 시작할 수 없습니다.
그러나 첫 글자 이후에는 숫자가 포함될 수 있습니다.
대문자와 소문자를 구분함 : 변수 이름을 명명시, 대문자와 소문자를 구분합니다.
예를 들어 int apple = 1;, int Apple = 2;, int APPLE = 3;은 모두 다른 변수로 취급됩니다.
공백을 허용하지 않음 : 변수 이름을 명명시 공백이 들어가서는 않됩니다.
예를 들어 int my friends = 7;과 같이 공백이 들어가서는 안됩니다.
특수 문자 제한 : ‘_(underscore)’와 ‘$’를 제외한 특수 문자는 변수명으로 사용할 수 없습니다.
예를 들어 ‘@’, ‘#’, ‘%’ 등은 변수 이름으로 사용할 수 없습니다.
자바 예약어 사용 금지 : int, class, static 등 자바에서 이미 의미를 갖는 예약어는 변수 이름으로 사용할 수 없습니다.
2️⃣ 표기법 및 관례(컨벤션)
카멜 케이스 : 첫 단어는 소문자로 시작하고, 이어지는 각 단어의 첫 글자는 대문자로 시작합니다.
예를 들어, firstName, totalAmount 등 입니다.
파스칼 케이스 : 각 문자의 첫 문자를 대문자로 표기합니다.
예를 들어, MyFriends, ToTalCount 등 입니다.
의미 있는 이름 : 변수 이름은 그 변수가 무엇을 의미하는지 명확하게 표현해야 합니다.
예를 들어, numberOfStudents 는 학생 수를, temperature는 온도를 나타내는 등의 명확한 이름을 사용하는 것이 좋습니다.
상수 이름 규칙 : 상수(변하지 않는 값)는 모두 대문자를 사용하며, 단어 사이는 밑줄(‘_‘)로 구분합니다.
예를 들어 MAX_HEIGHT, TOTAL_COUNT 등 입니다.
-
☕️[Java] 코테 맛보기(2) - 코테를 위한 자료구조와 알고리즘 개념 구현 방법 숙지
😋 코테 맛보기.
코딩 테스트를 준비하기 위해서 자료구조와 알고리즘은 매우 중요한 영역입니다.
이 분야들에 대한 깊이 있는 이해와 숙지는 테스트에서 성공적인 성과를 내는 데 결정적인 역할을 합니다.
다음은 코딩 테스트를 위해 필요한 자료구조와 알고리즘의 개념 및 구현 방법에 대한 가이드입니다.
1️⃣ 자료구조.
기본 자료구조 : 배열, 스택, 큐, 링크드 리스트.
이러한 자료구조들은 다양한 문제에서 데이터를 효율적으로 관리하는 기본적인 방법을 제공합니다.
고급 자료구조 : 트리(특히 이진 검색 트리), 힙, 그래프, 해시 테이블, 집합 등
이들은 보다 복잡한 데이터 관계를 다루는 데 사용되며, 특정 유형의 문제를 해결하는 데 특화되어 있습니다.
응용 자료구조 : 트라이, 세그먼트 트리, 유니온 파인드, 비트마스크 등.
이들은 특정 알고리즘 문제에 최적화된 솔루션을 제공합니다.
2️⃣ 알고리즘.
정렬 알고리즘 : 버블 정렬, 삽입 정렬, 선택 정렬, 퀵 정렬, 병합 정렬 등.
정렬은 많은 문제에서 데이터를 조작하는 기본적인 방법입니다.
검색 알고리즘 : 선형 검색, 이진 검색 등
데이터 내에서 특정 항목을 찾는 방법입니다.
재귀 알고리즘과 백트래킹 : 문제를 더 작은 문제로 나누어 해결하는 기법입니다.
동적 프로그래밍 : 복잡한 문제를 간단한 하위 문제로 나누어 해결하고, 그 결과를 저장하여 효율적으로 최종 결과를 도출합니다.
그래프 알고리즘 : 깊이 우선 탐색(DFS), 너비 우선 탐색(BFS), 최단 경로 문제(다익스트라, 플로이드-워셜), 최소 신장 트리(크루스칼, 프림) 등을 포함합니다.
3️⃣ 코테 준비 방법.
이론 학습 : 자료구조와 알고리즘의 이론을 철저히 학습합니다.
이론적인 이해는 효과적인 구현의 기초가 됩니다.
실습 연습 : 이론을 바탕으로 다양한 문제를 실제로 코딩해 봄으로써 실력을 키웁니다.
LeetCode, HackerRank, Codeforces, 백준, 프로그래머스 등의 플랫폼에서 문제를 풀어봅니다.
알고리즘 패턴 학습 : 자주 출제되는 문제 유형과 그에 대한 표준적인 해결 방법을 익힙니다.
시간 관리 연습 : 코딩 테스트에서는 제한된 시간 내에 문제를 해결해야 하므로, 시간 관리 능력을 향상시킬 필요가 있습니다.
📝 정리.
코테를 위한 준비 과정에서 이러한 자료구조와 알고리즘에 대한 이해와 숙련도는 문제를 정확하고 효율적으로 해결할 수 있는 능력을 직접적으로 높여 줍니다.
따라서, 이 분야에 대한 철저한 준비와 연습을 통해 자신감을 갖고 테스트에 임할 수 있습니다.
-
☕️[Java] 코테 맛보기(1) - 코테를 위한 자바 프로그래밍 언어 사용 숙련도
😋 코테 맛보기.
코딩 테스트를 위해 자바 프로그래밍 언어를 사용하려면 몇 가지 중요한 요소에 숙력도를 갖추어야 합니다.
자바는 많은 기업들이 코딩 테스트에 사용하는 언어 중 하나이며, 효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
효과적인 문제 해결을 위해 다음과 같은 능력을 개발하는 것이 중요합니다.
1️⃣ 기본 문법 숙지.
자바의 기본 문법과 프로그래밍 구조에 익숙해져야 합니다.
변수, 데이터 타입, 연산자, 제어문(if, for, while 등), 메소드 호출 등 기본적인 구성 요소를 이해하고 사용할 수 있어야 합니다.
2️⃣ 객체 지향 프로그래밍(OOP)이해.
자바는 객체 지향 프로그래밍 언어입니다.
클래스, 객체, 상속, 다형성, 캡슐화 등의 객체 지향 개념을 이해하고 이를 문제 해결에 적절히 적용할 수 있어야 합니다.
OOP 개념은 코드의 재사용성과 모듈성을 높여줘 효율적인 프로그래밍을 가능하게 합니다.
3️⃣ 표준 라이브러리 사용.
자바의 표준 라이브러리에는 다양한 자료구조와 알고리즘이 구현되어 있습니다.
java.util 패키지 내의 컬렉션 프레임워크(리스트, 맵, 셋 등)를 비롯해, 유용한 유틸리티 클래스들을 활용할 줄 알아야 합니다.
이러한 라이브러리들은 코딩 테스트에서 효율적인 코드 작성을 돕습니다.
4️⃣ 알고리즘과 자료구조.
다양한 알고리즘과 자료구조에 대한 이해가 중요합니다.
정렬, 탐색, 그래프 이론, 동적 프로그래밍 등의 알고리즘과 배열, 스택, 큐, 링크드 리스트, 트리 등의 자료구조에 대한 깊은 이해가 필요합니다.
이는 문제를 효과적으로 분석하고 최적의 해결책을 구현하는 데 결정적입니다.
5️⃣ 문제 해결 능력.
실제 코딩 테스트에서는 다양한 유형의 문제가 제시됩니다.
문제를 빠르게 이해하고 효과적인 해결책을 설계할 수 있는 능력이 필요합니다.
이는 실전 연습을 통해 향상시킬 수 있으며, 온라인 코딩 플랫폼에서 다양한 문제를 풀어 보는 것이 좋습니다.
6️⃣ 테스트와 디버깅
코드가 예상대로 동작하는지 검증하고, 오류를 찾아 수정할 수 있는 능력도 중요합니다.
자바에서 제공하는 디버깅 도구를 사용하여 코드를 단계별로 실행하고, 변순의 상태를 확인하며 문제를 진단할 수 있어야 합니다.
📝 정리.
코딩 테스트를 위한 자바 숙련도는 이론적 지식과 실제 적용 능력의 조합을 요구합니다.
이를 위해 개념 학습과 함께 많은 실습을 병행하는 것이 중요합니다.
시간을 정해두고 실전처럼 문제를 풀어보는 연습을 꾸준히 하면, 효과적으로 자바를 활용하여 코딩 테스트에서 좋은 성과를 낼 수 있을 것입니다.
-
☕️[Java] 자바 - 소개
자바 - 소개
🙋♂️ 1. 자바의 특징.
자바는 세계적으로 널리 사용되는 프로그래밍 언어로, 웹 개발, 모바일 애플리케이션, 대규모 시스템 구축 등 다양한 분야에 활용됩니다.
자바의 주요 특징들은 다음과 같습니다.
1️⃣ 플랫폼 독립성.
“Write Once, Run Anywhere”(WORA) : 자바 프로그램은 자바 가상 머신(JVM) 위에서 실행되기 때문에, 한 번 작성하면 어떤 플랫폼에서도 실행할 수 있습니다.
이는 자바 컴파일러가 소스 코드를 플랫폼 독립적인 바이트코드로 변환하기 때문입니다.
2️⃣ 객체 지향 프로그래밍(OOP).
자바는 객체 지향 프로그래밍 언어로, 캡슐화, 상속, 다형성 등을 완전히 지원합니다.
이는 코드 재사용, 유지 관리의 용이성 및 시스템 모듈화를 가능하게 합니다.
3️⃣ 강력한 표준 라이브러리.
자바는 방대한 표준 라이브러리를 제공하여, 네트워킹, 파일 시스템 접근, 그래픽 인터페이스 제작 등 다양한 작업을 쉽게 처리할 수 있도록 돕습니다.
4️⃣ 메모리 관리.
자동 가비지 컬렉션
자바는 사용하지 않는 객체를 자동으로 감지하고 메모리에서 제거하는 가비지 컬렉터를 내장하고 있습니다. 이는 개발자가 메모리 누수에 대해 걱정할 필요가 적어지게 해줍니다.
5️⃣ 보안.
자바는 샌드박스 환경에서 애플리케이션을 실행하여 시스템 리소스에 대한 무단 접근을 방지합니다.
또한, 클래스 로더, 바이트코드 검증기 등을 통해 애플리케이션이 안전하게 실행될 수 있도록 합니다.
6️⃣ 멀티스레딩.
자바는 내장된 멀티스레딩 기능을 지원하여, 여러 스레드가 동시에 실행되도록 하여 애플리케이션의 효율성을 높입니다.
이는 특히 네트워크 서버와 실시간 시스템에서 큰 장점입니다.
7️⃣ 로버스트와 포터빌리티.
자바 프로그램은 다른 플랫폼으로의 이동성이 뛰어나며, 높은 수준의 안정성을 제공합니다.
예외 처리 기능을 통해 오류를 쉽게 관리하고, 시스템의 안정성을 높일 수 있습니다.
📝 마무리.
자바의 이러한 특징들은 그것을 매우 유연하고, 다양한 애플리케이션 개발에 적합하게 만듭니다.
이로 인해 자바는 세계적으로 인기 있는 프로그래밍 언어 중 하나로 자리 잡게 되었습니다.
🙋♂️ 2. 자바 프로그램의 작성과 실행과정.
1️⃣ 소스 코드 작성.
개발자는 자바의 문법에 맞추어 .java 확장자 파일에 소스 코드를 작성합니다.
이 파일에는 하나 이상의 클래스가 포함되며, 각 클래스는 데이터와 메서드를 정의합니다.
2️⃣ 컴파일.
소스 코드 파일을 자바 컴파일러(javac)를 사용하여 컴파일합니다.
컴파일러는 소스 코드를 읽고, 문법 오류를 검사한 후, 바이트코드라는 중간 형태의 코드로 변환합니다.
이 바이트 코드는 .class 파일로 저장됩니다.
바이트코드는 플랫폼 독립적이기 때문에, 한 번 컴파일된 .class 파일은 다양한 운영 체제에서 실행될 수 있습니다.
3️⃣ 로딩.
자바 가상 머신(JVM)은 .class 파일을 로드합니다.
클래스 로더(component of JVM)가 이 작업을 수행하며, 필요한 클래스 파일들을 메모리에 로드합니다.
4️⃣ 링킹.
로드된 클래스 파일들은 링킹 과정을 거칩니다. 링킹은 검증, 준비, 그리고(선택적으로) 해석 단계를 포함합니다.
검증 : 로드된 바이트코드가 올바르게 포맷되었는지, 안전한지 검사합니다.
준비 : 클래스 변수와 기본값을 위한 메모리를 할당합니다.
해석 : 심볼릭 메모리 참조를 직접 참조로 변환합니다(선택적).
5️⃣ 초기화.
클래스 초기화 단계에서 정적 변수들에 대한 초기화가 수행되며, 정적 블록이 실행됩니다.
6️⃣ 실행.
프로그램 실행 동안 JVM 내부에서 가비지 컬렉터가 사용되지 않는 객체를 자동으로 감지하고, 할당된 메모리를 해제하여 메모리를 관리합니다.
📝 마무리.
자바의 이러한 실행 과정은 코드의 플랫폼 독립성을 보장하고, 안정적이며 보안적인 실행 환경을 제공합니다.
이 모든 과정은 개발자로부터 대부분 숨겨져 있으며, 개발자는 주로 소스 코드 작성과 일부 디버깅에 집중할 수 있습니다.
-
-
☕️[Java] 자바란?
자바란?
자바 언어 특징.
1. 타 언어에 비해 배우기 쉽습니다.
2. 플랫폼에 독립적입니다.
- 자바 언어가 플랫폼에 독립적인 이유는 그 설계 철학과 메커니즘에 근거합니다.
- 자바는 "한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)" 라는 철학을 실현하기 위해 개발되었습니다.
- 이를 가능하게 하는 핵심 요소는 자바 가상 머신(Java Virtual Machine, JVM)과 자바 바이트코드의 도입입니다.
자바의 플랫폼 독립성의 주요 요인
1. 자바 가상 머선(JVM)
JVM은 자바 바이트 코드를 실행할 수 있는 런타임 환경을 제공합니다. 자바 프로그램이 컴파일되면, 플랫폼에 독립적인 바이트코드로 변환됩니다.
이 바이트코드는 어떤 특정 하드웨어나 운영 체제의 기계어 코드가 아닌, JVM이 이해할 수 있는 중간 형태의 코드입니다.
JVM은 바이트코드를 받아 각 플랫폼에 맞는 기계어 코드로 변환하고 실행합니다.
따라서, 자바 애플리케이션은 다양한 운영 체제에서 JVM만 설치되어 있으면 실행될 수 있습니다.
2. 컴파일과 실행의 분리
자바 프로그램은 소스 코드(.java 파일)에서 바이트코드(.class 파일)로 컴파일되는 과정과, 실행 시 바이트 코드가 실제로 실행되는 과정으로 나누어집니다. 이 두 단계의 분리는 프로그램을 한 번 컴파일하면, 그 컴파일된 코드가 다양한 환경의 JVM에서 실행될 수 있게 합니다.
3. 표준화된 API
자바는 풍부하고 표준화된 API를 제공합니다. 이 API들은 플랫폼에 관계없이 일관된 방식으로 작동하므로, 개발자는 운영 체제의 특징을 신경 쓰지 않고도 애플리케이션을 개발할 수 있습니다. 예를 들어, 파일 시스템 접근, 네트워크 프로그래밍 등의 기능은 모든 플랫폼에서 동일한 자바 코드로 작동합니다.
4. 언어와 라이브러리의 독립성
자바 언어와 표준 라이브러리는 플랫폼에 특화된 구현으로부터 독립적입니다.
즉, 자바의 표준 라이브러리 구현은 다양한 하드웨어와 운영 체제에서 동일하게 작동하도록 설계되었습니다.
3. 객체지향 프로그래밍입니다.
객체지향 프로그래밍?
자바에서의 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 소프트웨어를 설계하고 구현할 때 객체라는 개념을 중심으로 프로그래밍하는 방식을 말합니다.
객체지향 프로그래밍은 코드의 재사용성, 확장성 및 관리 용이성을 높이는 데 도움이 됩니다.
자바는 객체지향 언어의 특징을 강하게 반영하고 있으며, 다음과 같은 기본 원칙에 따라 프로그래밍 됩니다.
1. 캡슐화(Encapsulation)
객체의 데이터(속성)와 그 데이터를 조작하는 메소드를 하나의 단위로 묶는 것을 말합니다.
캡슐화를 사용하면 객체의 세부 구현 내용을 외부에서 알 필요 없이 객체가 제공하는 기능만을 사용할 수 있으며, 이는 코드의 유지보수를 용이하게 합니다.
2. 상속(Inheritance)
한 클래스가 다른 클래스의 특성을 상속 받아 사용할 수 있게 하는 것입니다.
이를 통해(상속을 통해) 기존 코드를 재사용하면서 확장할 수 있고, 코드의 중복을 줄이며 유지 보수가 쉬워집니다.
3. 다형성(Polymorphism)
같은 이름의 메소드가 다른 작업을 수행할 수 있도록 하여 메소드의 오버라이딩(Overriding)이나 오버로딩(Overloading)을 가능하게 합니다.
오버라이딩(Overriding) : 자식 클래스가 상속 받은 부모 클래스의 메소드를 재정의 하는 행위를 말합니다. 오버라이딩을 통해 자식 클래스는 상속 받은 메소드와 동일한 시그니처(메소드 이름, 매개변수 리스트)를 가지지만, 그 내용을 자신의 특정한 요구에 맞게 새롭게 구현할 수 있습니다. 오버라이딩된 메소드는 실행 시 다형성을 활용하여 해당 객체의 실제 타입에 따라 적절한 메소드가 호출됩니다.
예시 코드
java
class Animal {
void display() {
System.out.println("This is an animal.");
}
}
class Cat extends Animal {
@Override
void display() {
System.out.println("This is a cat.")
}
}
오버로딩(Overloading) : 같은 클래스 내에서 같은 이름의 메소드를 여러 개 정의할 수 있도록 하지만, 매개변수의 타입, 개수 또는 순서가 달라야 합니다. 이를 통해 메소드에 다양한 입력 파라미터를 제공할 수 있으며, 프로그래머가 같은 동작을 하는 메소드에 대해 다양한 옵션을 제공할 수 있습니다. 오버로딩은 컴파일 시간에 결정되며, 메소드 호출 시 전달된 매개변수에 따라 적절한 메소드가 선택됩니다.
예시 코드
class Display {
void show(int a) {
System.out.println("Number: " + a);
}
void show(String a) {
System.out.println("String: " + a);
}
void show(int a, int b) {
System.out.println("Two numbers: " + a + ", " + b);
}
}
이처럼 오버라이딩과 오버로딩은 자바 프로그래밍에서 메소드의 기능을 확장하거나 변경할 때 유용하게 쓰이는 기법입니다.
오버라이딩은 주로 다형성을 활용한 동적 바인딩을 목적으로 하며, 오버로딩은 같은 이름의 메소드에 여러 입력 형태를 제공하기 위해 사용됩니다.
4. 추상화(Abstraction)
복잡한 실제 상황을 단순화하는 과정에서 중요한 특징만을 추출하여 프로그램 코드에 반영하는 것을 의미합니다.
추상 클래스와 인터페이스를 통해 구현될 수 있습니다.
이러한 원칙들은 자바를 사용하여 복잡한 시스템을 개발할 때 코드의 모듈화를 가능하게 하고, 이로 인해 대규모 소프트웨어 개발과 프로젝트 관리가 용이해집니다.
코드의 모듈화(Modularization): 큰 프로그램을 작은 세부 모듈로 나누는 프로세스를 의미합니다. 이러한 모듈은 각각 독립적인 기능을 수행하며, 전체 시스템의 한 부분으로 기능합니다. 모듈화의 주요 목적은 프로그램의 관리를 용이하게 하고, 개발을 효율적으로 만들며, 코드의 재사용성을 높이는 것입니다.
모듈화의 주요 이점은 다음과 같습니다.
1. 유지보수성
모듈화된 코드는 각 모듈이 분리되어 있기 때문에, 하나의 모듈에서 발생한 문제가 다른 모듈에 미치는 영향을 최소화할 수 있습니다. 따라서 개별 모듈을 독립적으로 수정, 업데이트, 테스트할 수 있어 전체 코드베이스의 유지보수가 더 쉬워 집니다.
모듈(Module): 소프트웨어 설계에서 사용되는 기본 개념 중 하나로, 관련된 기능들을 논리적으로 그룹화하고 독립적으로 사용할 수 있는 코드의 단위를 의미합니다. 모듈은 프로그램의 특정 기능을 담당하며, 독립적인 개발, 테스트, 재사용이 가능하도록 설계됩니다. 모듈화된 코드는 대체로 명확하고 관리하기 쉬운 구조를 갖습니다.
모듈의 특징으로는 다음과 같습니다.
1. 독립성
모듈은 가능한 한 다른 모듈과 독립적으로 동작할 수 있어야 하며, 이를 통해 시스템의 복잡성을 줄이고, 각 모듈의 재사용성을 높일 수 있습니다.
2. 캡슐화
모듈은 자신의 구현 세부사항을 숨기고, 필요한 기능만을 외부에 제공하는 인터페이스를 통해 상호작용합니다. 이로 인해 모듈 간의 상호 의존성이 줄어들고, 변경 관리가 용이해집니다.
3. 인터페이스
모듈은 정의된 인터페이스를 통해 외부와 통신합니다. 인터페이스는 모듈이 제공하는 기능과 해당 기능을 어떻게 접근할 수 있는지를 명시합니다.
모듈의 예로는 다음으로 들 수 있습니다.
라이브러리
특정 기능을 제공하는 함수나 데이터 구조를 모아 놓은 코드 집합. 예를 들어, 수학 연산을 위한 수학 라이브러리, 데이터베이스 작업을 위한 데이터베이스 접근 라이브러리 등이 있습니다.
클래스
객체지향 프로그래밍에서 클래스는 속성(데이터)과 메소드(함수)를 캡슐화하여 모듈을 형성합니다. 클래스는 독립적으로 사용될 수 있으며, 다른 클래스와 상호작용할 수 있습니다.
패키지
관련된 여러 클래스나 모듈을 하나의 더 큰 단위로 그룹화한 것 입니다. 예를 들어, Java에서는 java.util 패키지가 여러 유틸리티 클래스와 인터페이스를 제공합니다.
모듈은 개발 과정을 체계화하고, 코드의 재사용성을 증가시키며, 유지 관리를 용이하게 하는 중요한 역할을 합니다.
모듈은 크기가 클 수도 있고 작을 수도 있으며, 프로젝트의 요구와 설계에 따라 그 범위와 기능이 결정됩니다.
2. 재사용성
잘 설계된 모듈은 다른 프로그램에서도 재사용할 수 있습니다. 이는 소프트웨어 개발 시간과 비용을 줄이는 데 도움이 되며, 일관된 기능을 여러 프로젝트에 걸쳐 사용할 수 있습니다.
3. 확장성
모듈화는 시스템의 확장성을 향상시킵니다. 새로운 기능이 필요할 때 기존 모듈을 수정하거나 새로운 모듈을 추가하기가 더 쉬워집니다. 이는 시스템의 유연성을 증가시키고, 변화하는 요구사항에 더 잘 대응할 수 있게 합니다.
4. 가독성
작은 모듈로 나뉘어진 코드는 각각의 모듈이 명확한 기능을 수행하기 때문에, 전체 코드의 구조를 이해하기가 더 쉽습니다. 개발자가 프로그램의 특정 부분만을 이해하고도 효과적으로 작업할 수 있습니다.
5. 팀 협업 향상
모듈화는 여러 개발자가 동시에 다른 모듈에서 작업할 수 있게 함으로써 팀 작업을 용이하게 합니다. 각 팀원이 특정 모듈에 집중할 수 있으며, 전체 프로젝트에 대한 의존성을 줄이면서 협업을 효율적으로 진행할 수 있습니다.
이처럼 코드의 모듈화는 소프트웨어 개발 과정에서 중요한 역할을 하며, 특히 대규모 프로젝트나 복잡한 시스템 개발에 있어 필수적인 접근 방식입니다.
4. Garbage Collector로 사용되지 않는 메모리를 자동적으로 정리해줍니다.
Garbage Collector(GC): 프로그램이 동적으로 할당한 메모리 영역 중에서 더 이상 사용하지 않는 부분을 자동으로 찾아서 해제하는 시스템을 말합니다. 이 과정을 통해 프로그램에서 발생할 수 있는 메모리 누수를 방지하고, 사용 가능한 메모리 리소스를 최적화합니다.
프로그램이 동적으로 할당한 메모리 영역 : 프로그램 실행 중에 필요에 따라 할당되고 해제되는 메모리를 말합니다. 이는 프로그램의 런타임 중에 사용자의 요구나 데이터의 양에 따라 변화하는 메모리 요구 사항을 수용하기 위해 사용됩니다. 동적 메모리 할당은 프로그램이 시작할 때 필요한 메모리 양을 미리 알 수 없는 경우나, 실행 도중에 메모리 사용량이 변할 때 유용합니다.
동적 메모리 할당의 특징은 아래와 같습니다.
1. 유연성 : 동적 메모리 할당은 프로그램 실행 중에 필요한 메모리 크기를 조정할 수 있게 해줍니다. 이로 인해 프로그램은 사용자의 입력, 파일 크기, 또는 다른 실행 시 요소들에 따라 메모리 사용을 최적화할 수 있습니다.
2. 효율성 : 필요할 때만 메모리를 할당하고, 더 이상 사용하지 않는 메모리를 해제함으로써 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.
3. 메모리 관리 : 동적 메모리는 일반적으로 힙(Heap) 영역에서 관리됩니다. 힙은 프로그램의 데이터 영역 중 하나로, 동적으로 할당되는 객체와 데이터에 사용됩니다. 힙 영역의 크기는 프로그램 실행 도중에 확장되거나 축소될 수 있습니다.
동적 메모리 할당의 예는 다음과 같습니다.
자바에서는 new 키워드를 사용하여 객체를 생성할 때 동적 메모리 할당이 일어납니다. 예를 들어, new ArrayList() 를 호출하면, 자바 런타입은 필요한 메모리를 힙에서 할당하여 ArrayList 객체를 저장합니다.
객체 사용이 끝나면 자바의 GC가 더 이상 참조되지 않는 객체가 사용하던 메모리를 자동으로 해제합니다.
동적 메모리 할당은 프로그램이 더 유연하고 효율적으로 동작하도록 돕지만, 관리가 제대로 이루어지지 않을 경우 메모리 누수나 성능 저하 같은 문제를 초래할 수 있습니다. 따라서 프로그래머는 동적 메모리 관리를 신중하게 수행해야 합니다.
GC의 주요 기능은 다음과 같습니다.
1. 메모리 관리 자동화 : 프로그래머가 메모리 할당 및 해제를 직접 관리하는 대신 자바 런타입이 이를 자동으로 처리합니다. 이로 인해 개발자는 메모리 관리에 신경 쓰지 않고, 애플리케이션 로직 개발에 더 집중할 수 있습니다.
2. 메모리 누수 방지 : GC는 참조되지 않는 객체들을 정기적으로 청소하여 메모리 누수를 방지합니다. 객체가 더 이상 필요 없을 때 자동으로 메모리에서 제거됩니다.
3. 효율적인 메모리 사용 : 사용되지 않는 객체들을 정리함으로써 메모리를 효율적으로 사용하고, 애플리케이션의 성능을 유지할 수 있도록 도와줍니다.
GC의 작동 원리는 다음과 같습니다.
GC은 크게 두 단계로 진행됩니다.
1. 객체 탐지 : GC는 더 이상 어떤 객체에도 참조되지 않는 객체들을 탐지합니다. 이러한 객체들은 프로그램에서 더 이상 사용되지 않는 것으로 간주됩니다.
2. 메모리 회수 : 탐지된 객체들이 차지하고 있는 메모리를 해제합니다. 이 메모리는 다시 사용 가능한 상태가 되어, 새로운 객체를 위해 재할당될 수 있습니다.
GC 알고리즘
자바는 다양한 GC 알고리즘을 제공합니다. 대표적인 몇 가지는 다음과 같습니다.
Mark-and-Sweep : 사용 중인 객체를 “표시(mark)”하고, 표시되지 않은 객체를 “쓸어내는(sweep)” 방식입니다.
Generational GC : 객체를 세대별로 분류하여, 생성된지 얼마 되지 않은 객체들(Young Generation)과 오래된 객체들(Old Generation)을 다르게 관리합니다. 이 방식은 대부분의 객체가 생성 후 짧은 시간 내에 소멸된다는 관찰에 기반합니다.
Compacting : 사용 중인 객체들을 메모리의 한쪽으로 몰아넣어(Compact), 메모리의 연속성을 높이고, 메모리 단편화를 방지합니다.
GC은 메모리 관리를 자동화하지만, 때로는 성능 저하를 일으킬 수 있습니다. 특히 GC가 실행되는 동안에는 프로그램의 다른 모든 작업이 일시적으로 중단(Stopping the world)될 수 있기 때문에, GC 동작 방식과 설정을 잘 이해하고 조절하는 것이 중요합니다.
JVM(Java Virtual Machine)
JVM은 자바 애플리케이션을 실행하기 위한 가상 머신으로, 자바 바이트코드를 로컬 기계 코드로 변환하여 실행하는 역할을 합니다.
자바 바이트코드(Java Bytecode) : 자바 소스 코드가 컴파일된 후의 중간 형태입니다.
자바 소스 파일(.java 파일)을 자바 컴파일러가 컴파일하면, 결과적으로 생성되는 것이 .class 파일로 저장되는 자바 바이트코드입니다.
이 바이트코드는 기계어 코드는 아니지만, CPU가 직접 실행할 수는 없고, JVM이 이해하고 실행할 수 있는 명령어 세트로 구성되어 있습니다.
바이트코드는 플랫폼에 독립적이기 때문에, 한 번 컴파일된 자바 프로램은 어떤 JVM이 설치된 시스템에서든 실행할 수 있습니다.
이는 자바의 “한 번 작성하면, 어디서든 실행된다”라는 이점을 제공합니다.
로컬 기계 코드(Local Machine Code) : 로컬 기계 코드는 특정 하드웨어 플랫폼의 CPU가 직접 이해하고 실행할 수 있는 명령어 코드입니다.
이 코드는 플랫폼에 종속적이며, 다양한 운영 체제와 하드웨어 아키텍처는 각각의 기계어 코드를 가지고 있습니다.
자바 바이트코드는 JVM을 통해 실행될 때, 두 가지 방법 중 하나로 실행될 수 있습니다.
1. 인터프리터 : JVM은 바이트코드를 한 줄씩 읽고, 각 명령을 로컬 기계 코드로 변환하면서 실행합니다. 이 방법은 간단하지만, 실행 속도가 느릴 수 있습니다.
2. JIT 컴파일러(Just-In-Time Compiler) : 이 방식에서는 JVM이 바이트코드 전체 또는 핵심 부분을 분석하여, 실행 전에 전체 코드를 로컬 기계 코드로 한번에 변환합니다. 이렇게 하면 프로그램의 실행 속도가 크게 향상됩니다.
결국, 자바 바이트코드는 플랫폼 독립적인 중간 코드로서의 역할을 하며, 로컬 기계 코드는 실제 하드웨어에서 실행되기 위한 최종적인 코드 형태입니다. 이 두 코드의 변환과 실행은 JVM 내에서 처리되며, 사용자는 이 과정을 명시적으로 관리할 필요가 없습니다. 이것이 자바가 제공하는 큰 이점 중 하나입니다.
JVM은 자바의 “한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere, WORA)” 라는 철학을 가능하게 하는 중요한 구성 요소입니다.
JVM 덕분에 자바 애플리케이션은 운영 체제나 하드웨어 플랫폼에 구애받지 않고 동일하게 실행될 수 있습니다.
JVM의 주요 기능.
1. 플랫폼 독립성 : 자바 프로그램은 JVM 위에서 실행되므로, JVM이 설치되어 있는 모든 운영 체제에서 같은 자바 프로그램을 실행할 수 있습니다.
이는 JVM이 플랫폼에 특화된 코드로 바이트 코드를 변환하기 때문입니다.
2. 메모리 관리 : JVM은 자동 메모리 관리 기능을 제공합니다. 이는 GC를 통해 메모리 할당과 해제를 관리하여, 프로그래머가 메모리 누수 없이 효율적인 메모리 사용을 할 수 있도록 돕습니다.
3. 보안 : 자바 바이트코드는 JVM에 의해 검증되며 실행되기 전에 다양한 검사를 통해 안전성이 확보됩니다.
이는 악의적인 코드 실행과 시스템 오류를 방지하는 데 도움이 됩니다.
4. 실행 환경 : JVM은 자바 애플리케이션에 필요한 실행 환경을 제공합니다.
이 환경은 클래스 로더, 바이트코드 실행 엔진, 쓰레드 관리 등을 포함합니다.
JVM의 구성 요소.
1. 클래스 로더(Class Loader) : 클래스 파일들을 읽고 바이트코드를 JVM 메모리에 로드하는 역할을 합니다.
2. 실행 엔진(Excution Engine) : 로드된 클래스 파일의 바이트코드를 실행합니다. 이 엔진은 바이트코드를 해것하거나 필요에 따라 JTI(Just-In-Time) 컴파일러를 사용하여 바이트코드를 직접 기계 코드로 변환하여 실행 속도를 높일 수 있습니다.
3. 가비지 컬렉터(Garbage Collector) : JVM이 사용하지 않는 메모리 자원을 자동으로 회수합니다.
4. 메모리(Runtime Data Area) : JVM은 프로그램 실행을 위해 필요한 다양한 메모리 영역을 관리합니다. 이는 힙(Heap), 스택(Stack), 메소드 영역(Method Area), 프로그램 카운터(Program Counter) 등이 포함됩니다.
-
[AnD] 두 수의 합.
문제 설명 🤓
0 이상의 두 정수가 문자열 a, b로 주어질 때, a + b의 값을 문자열로 return 하는 solution 함수를 작성해 주세요.
솔루션 📝
import java.math.BigInteger;
class Solution {
public String solution(String a, String b) {
String answer = "";
BigInteger bigNumberA = new BigInteger(a);
BigInteger bigNumberB = new BigInteger(b);
answer = bigNumberA.add(bigNumberB).toString();
return answer;
}
}
트러블슈팅 🏀
1. NumberFormatException 에러(1).
입출력의 예시 중 가장 긴 입력 예시인 a : “18446744073709551615”, b : “305793246910280479981” 에서 에러가 발생 했습니다.
1️⃣ 콘솔에 나타난 에러 메시지
콘솔에 나타난 에러 메시지는 아래와 같았습니다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "18446744073709551615"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.valueOf(Integer.java:989)
at programmers.test1.Solution.solution(Solution.java:8)
at programmers.test1.SolutionMain.main(SolutionMain.java:7)
Process finished with exit code 1
2️⃣ 본격적인 트러블슈팅
이 메시지를 하나씩 해석하고 트러블슈팅을 이어갔습니다.
먼저 이 오류 메시지는 NumberFormatException 이 발생했다는 것을 나타냅니다.
특히 “For input string: “18446744073709551615”는 Java에서 정수로 변환하려는 문자열이 정부 범위를 벗어났음을 의미합니다.
Java의 Integer.parseInt() 메소드는 문자열을 정수(Int)로 변환할 때 사용됩니다.
그러나 Int 자료형은 -2,147,483,648,648 부터 2,147,483,648,647까지의 값을 저정할 수 있습니다.
제공된 문자열 “18446744073709551615”는 이 범위를 훨씬 초과합니다.
3️⃣ 해결 방법
이 문제를 해결하려면 다음과 같은 방법을 고려할 수 있습니다.
1. 타입 변경
int 대신 long 타입을 사용하거나, 이보다 더 큰 범위가 필요하다면, BigInteger 클래스를 사용할 수 있습니다.
long 의 범위는 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807 까지입니다.
2. 입력 검증
입력 값이 정수 타입으로 변환 가능한지, 그리고 해당 타입의 범위 내에 있는지 검증하는 로직을 추가합니다.
코드를 수정할 때는 적절한 데이터 타입을 사용하도록 주의해야 합니다.
예를 들어, long 으로 변경하려면 Long.parseLong() 을 사용할 수 있습니다.
2. NumberFormatException 에러(2)
이번에는 위의 트러블슈팅을 활용하여 코드를 만든 결과 NumberFormatException 에러를 다시 발생 시킨 케이스 입니다.
이번에는 Long.parseLong() 메소드를 사용하면서 발생했습니다.
문자열 “18446744073709551615”는 이번에도 범위를 벗어난 값으로 처리되었습니다.
long 자료형의 최대값은 9,223,372,036,854,775,807dlqslek.
제공된 값 “18446744073709551615”는 이 최대값을 초과합니다.
따라서, long 으로도 처리할 수 없으며, Java에서 이러한 큰 숫자를 다루려면 BigInteger 클래스를 사용해야 합니다.
BigInteger 는 사실상 제한 없는 정밀도의 정수를 다룰 수 있어 이와 같은 큰 숫자를 취급할 때 유용합니다.
BigInteger를 사용하는 예시 코드.
import java.math.BigInteger;
public class Solution {
public void solution(String input) {
BigInteger bigNumber = new BigInteger(input);
// bigNumber를 사용한 다른 로직
}
}
public class SolutionMain {
public static void main(String[] args) {
new Solution().solution("18446744073709551615");
}
}
이 코드는 BigInteger 를 사용하여 입력된 숫자를 처리하고, 필요한 로직을 수행할 수 있도록 구성되어 있습니다.
3. BigInteger 클래스를 사용하여 두 큰 정수를 더하는 방법.
두 문자열을 받아 큰 범위의 문자열을 BigInteger 클래스를 사용하여 받아오고 변환하는 데 까지는 성공하였으나 입력된 두 개의 큰 범위 값의 BigInteger 를 어떻게 합쳐야 할지를 몰라 검색해 봤습니다.
1️⃣ Java에서 BigInteger 클래스를 사용하여 두 큰 정수를 더하는 방법
BigInteger 클래스는 불변(immutable) 객체 이므로 두 BigInteger 인스턴스를 더할 때, 새로운 BigInteger 객체가 반환됩니다.
2️⃣ BigInteger 객체를 더하는 방법 예시 코드
import java.math.BigInteger;
public class Main {
public static void main(String[] args) {
// 두 큰 수를 BigInteger로 생성
BigInteger number1 = new BigInteger("12345678901234567890");
BigInteger number2 = new BigInteger("98765432109876543210");
// 두 수를 더함
BigInteger sum = number1.add(number2);
// 결과 출력
System.out.println("Sum: " + sum.toSting())
}
}
이 코드는 다음과 같은 단계를 거칩니다.
1. 두 개의 BigInteger 인스턴스 number1 과 number2 를 생성합니다.
이들은 문자열로부터 생성되며, 매우 큰 수를 나타낼 수 있습니다.
2. add 메소드를 사용하여 number1 과 number2 를 더합니다.
이 메소드는 두 수의 합을 나타내는 새로운 BigInteger 객체를 반환합니다.
3. 덧셈 결과를 출력합니다.
BigInteger 를 사용하면 정수의 범위에 제한 없이 수학적 연산을 수행할 수 있어, 매우 큰 수를 처리해야 할 때 유용합니다.
-
☕️[Java] Math, Random 클래스
Math, Random 클래스
Math 클래스
Math는 수 많은 수학 문제를 해결해주는 클래스입니다.
너무 많은 기능을 제공하기 때문에 대략 이런 것이 있구나 하는 정도면 충분합니다.
실제 필요할 때 검색하거나 API 문서를 찾아봅시다.
1. 기본 연산 메서드
abs(x) : 절대값
max(a, b) : 최대값
min(a, b) : 최소값
2. 지수 및 로그 연산 메서드
exp(x) : e^x 계산
log(x) : 자연 로그
log10(x) : 로그 10
pow(a, b) : a의 b 제곱
3. 반올림 및 정밀도 메서드
ceil(x) : 올림
floor(x) : 내림
rint(x) : 가장 가까운 정수로 반올림
round(x) : 반올림
4. 삼각 함수 메서드
sin(x) : 사인
cos(x) : 코사인
tan(x) : 탄젠트
5. 기타 유용한 메서드
sqrt(x) : 제곱근
cbrt(x) : 세제곱근
random() : 0.0과 1.0 사이의 무작위 값 생성
Math에서 자주 사용하는 기능들을 예제로 만들어서 실행해봅시다.
package lang.math;
public class MathMain {
public static void main(String[] args) {
System.out.println("max(10, 20): " + Math.max(10, 20)); // 최대값
System.out.println("min(10, 20): " + Math.min(10, 20)); // 최소값
System.out.println("abs(-10): " + Math.abs(-10)); // 절대값
// 반올림 및 정밀도 메서드
System.out.println("ceil(2.1): " + Math.ceil(2.1)); // 올림
System.out.println("floor(2.1): " + Math.floor(2.1)); // 내림
System.out.println("round(2.5): " + Math.round(2.5)); // 반올림
// 기타 유용한 메서드
System.out.println("sqrt(4): " + Math.sqrt(4)); // 제곱근
System.out.println("random(): " + Math.random()); // 0.0 ~ 1.0 사이의 double 값을 반환
}
}
실행 결과
max(10, 20): 20
min(10, 20): 10
abs(-10): 10
ceil(2.1): 3.0
floor(2.1): 2.0
round(2.5): 3
sqrt(4): 2.0
random(): 0.45992290098234856
참고 : 아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해봅시다.
Random 클래스
랜덤의 경우 Math.random()을 사용해도 되지만 Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있습니다.
참고로 Math.random()도 내부에서는 Random 클래스를 사용합니다.
참고로 Random 클래스는 java.util 패키지 소속입니다.
package lang.math;
import java.util.Random;
public class RandomMain {
public static void main(String[] args) {
Random random = new Random();
int randomInt = random.nextInt();
System.out.println("randomIntL " + randomInt);
double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d 사이값이 출력됨
System.out.println("randomDouble: " + randomDouble);
boolean randomBoolean = random.nextBoolean();
System.out.println("randomBoolean: " + randomBoolean);
// 범위 조회
int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력
System.out.println("0 ~ 9: " + randomRange1);
int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력
System.out.println("1 ~ 10: " + randomRange2);
}
}
실행 결과
실행 결과는 항상 다르다.
randomIntL -1662566800
randomDouble: 0.14362030528813963
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 10
random.nextInt(): 랜덤 int 값을 반환합니다.
nextDouble() : 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환합니다.
nextBoolean() : 랜덤 boolean 값을 반환합니다.
nextInt(int bound): 0 ~ bound 미만의 숫자를 랜덤으로 반환합니다. 예를 들어서 3을 입력하면 0, 1, 2를 반환합니다.
1부터 특정 숫자의 int 범위를 구하는 경우 nextInt(int bound)의 결과에 +1을 하면 됩니다.
씨드 - Seed
랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구합니다.
그런데 이 씨드 값이 같으면 항상 같은 결과가 출력됩니다.
// Random random = new Random();
Random random = new Random(1); // seed가 같으면 Random의 결과가 같다.
실행 결과
Seed가 같으면 실행 결과는 반복 실행해도 같습니다.
randomIntL -1155869325
randomDouble: 0.10047321632624884
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 5
new Random(): 생성자를 비워두면 내부에서 System.nanoTime()에 여러가지 복잡한 알고리즘을 섞어서 씨드값을 생성합니다.
따라서 반복 실행해도 결과가 항상 달라집니다.
new Random(int seed): 생성자에 씨드 값을 직접 전달할 수 있습니다. 씨드 값이 같으면 여러번 반복 실행해도 실행 결과가 같습니다.
이렇게 씨드 값을 직접 사용하면 결과 값이 항상 같기 때문에 결과가 달라지는 랜덤 값을 구할 수 없습니다.
하지만 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있습니다.
참고로 마인크래프트 같은 게임은 게임을 시작할 때 지형을 랜덤으로 생성하는데, 같은 씨드값을 설정하면 같은 지형을 생성할 수 있습니다.
-
💾 [CS] CISC와 RISC
CISC와 RISC.
명령어 파이프라이닝과 슈퍼스칼라 기법을 실제로 CPU에 적용하려면 명령어가 파이프라이닝에 최적화되어 있어야 합니다.
쉽게 말해 CPU가 파이프라이닝과 슈퍼스칼라 기법을 효과적으로 사용하려면 CPU가 인출하고 해석하고 실행하는 명령어가 파이프라이닝 하기 쉽게 생겨야 합니다.
‘파이프라이닝 하기 쉬운 명령어’란 무엇일까요?
명령어가 어떻게 생겨야 파이프라이닝에 유리할까요?
이와 관련해 CPU의 언어인 ISA와 각기 다른 성격의 ISA를 기반으로 설계된 CISC와 RISC를 알아봅시다.
명령어 집합
세상에는 수많은 CPU 제조사들이 있고, CPU마다 규격과 기능 만듦새가 다 다릅니다.
그러므로 CPU가 이해하고 실행하는 명령어들이 다 똑같지 않습니다.
물론 명령어의 기본적인 구조와 작동원리는 큰 틀에서 크게 벗어나지 않습니다.
그러나 명령어의 세세한 생김새, 명령어로 할 수 있는 연산, 주소 지정 방식 등은 CPU마다 조금씩 차이가 있습니다.
명령어 집합(instruction set) 또는 명령어 집합 구조(ISA: Instruction Set Architecture) : CPU가 이해할 수 있는 명령어들의 모음.
CPU마다 ISA가 다를 수 있습니다.
명령어 집합에 ‘구조’라는 단어가 붙은 이유는 CPU가 어떤 명령어를 이해하는지에 따라 컴퓨터 구조 및 설계 방식이 달라지기 때문입니다.
가령 인텔의 노트북 속 CPU는 x86 혹은 x86-64 ISA를 이해하고, 애플의 아이폰 속 CPU는 ARM ISA를 이해합니다.
x86(x86-64)과 ARM은 다른 ISA이기 때문에 인텔 CPU를 사용하는 컴퓨터와 아이폰은 서로의 명령어를 이해할 수 없습니다.
실행 파일은 명령어로 이루어져 있고 서로의 컴퓨터가 이해할 수 있는 명령어가 다르기 때문입니다.
x86은 32비트용, x86-64는 64비트용 x86 ISA입니다.
어셈블리어는 명령어를 읽기 편하게 표현한 언어입니다.
ISA가 다르다는 건 CPU가 이해할 수 있는 명령어가 다르다는 뜻입니다.
명령어가 달라지면 어셈블리어도 달라집니다.
다시 말해 같은 소스 코드로 만들어진 같은 프로그램이라 할지라도 ISA가 다르면 CPU가 이해할 수 있는 명령어도 어셈블리어도 달라진다는 것입니다.
예를 들어 보겠습니다.
동일한 소스 코드를 작성하고 ISA가 다른 컴퓨터에서 어셈블리어로 컴파일하면 아래와 같은 결과를 얻을 수 있습니다.
왼쪽은 x86-64 ISA, 오른쪽은 ARM ISA입니다.
똑같은 코드로 만든 프로그램임에도 CPU가 이해하고 실행할 수 있는 명령어가 달라 어셈블리어도 다른 것을 알 수 있습니다.
참고로 사용한 컴파일러에 따라서도 어셈블리어가 달라질 수 있는데, 위 예시에서는 gcc 11.2라는 동일한 컴파일러를 이용했습니다.
ISA가 같은 CPU끼리는 서로의 명령어를 이해할 수 있지만, ISA가 다르면 서로의 명령어를 이해하지 못합니다.
이런 점에서 볼 때 ISA는 일종의 CPU의 언어인 샘입니다.
CPU가 이해하는 명령어들이 달라지면 비단 명령어의 생김새만 달라지는게 아닙니다
ISA가 다르면 그에 따른 나비 효과로 많은 것이 달라집니다.
제어장치가 명령어를 해석하는 방식, 사용되는 레지스터의 종류와 개수, 메모리 관리 방법 등 많은 것이 달라집니다.
그리고 이는 곧 CPU 하드웨어 설계에도 큰 영향을 미칩니다.
ISA는 CPU의 언어임과 동시에 CPU를 비롯한 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속이라고도 볼 수 있습니다.
앞서 명령어 병렬 처리 기법들을 학습했습니다.
이를 적용하기에 용이한 ISA가 있고, 그렇지 못한 ISA도 있습니다.
다시 말해 명령어 파이프라인, 슈퍼스칼라, 비순차적 명령어 처리를 사용하기에 유리한 명령어 집합이 있고, 그렇지 못한 명령어 집합도 있습니다.
그렇다면 명령어 병렬 처리 기법들을 도입하기 유리한 ISA는 무엇일까요?
이와 관련해 현대 ISA의 양대 산맥인 CISC와 RISC에 대해 알아보겠습니다.
CISC
CISC(Complex Instruction Set Computer) : ‘복잡한 명령어 집합을 활용하는 컴퓨터’
여기서 ‘컴퓨터’를 ‘CPU’라고 생각해도 좋습니다.
이름 그대로 복잡하고 다양한 명령어들을 활용하는 CPU 설계 방식입니다.
ISA의 한 종류로 소개한 x86, x86-64는 대표적인 CISC 기반의 ISA입니다.
다양하고 강력한 기능의 명령어 집합을 활용하기 때문에 명령어의 형태와 크기가 다양한 가변 길이 명령어를 활용합니다.
메모리에 접근하는 주소 지정 방식도 다양해서 아주 특별한 상황에서만 사용되는 독특한 주소 지정 방식들도 있습니다.
다양하고 강력한 명령어를 활용한다는 말은 상대적으로 적은 수의 명령어로도 프로그램을 실행할 수 있다는 것을 의미합니다.
프로그램을 실행하는 명령어 수가 적다는 말은 ‘컴파일된 프로그램의 크기가 작다’는 것을 의미합니다.
같은 소스 코드를 컴파일해도 CPU마다 생성되는 실행 파일의 크기가 다를 수 있다는 것입니다.
이런 장점 덕분에 CISC는 메모리를 최대한 아끼며 개발해야 했던 시절에 인기가 높았습니다.
‘적은 수의 명령어만으로도 프로그램을 동작시킬 수 있다’는 점은 메모리 공간을 절약할 수 있다는 장점이기 때문입니다.
하지만 CISC에는 치명적인 단점이 있습니다.
활용하는 명령어가 워낙 복잡하고 다양한 기능을 제공하는 탓에 명령어의 크기와 실행되기까지의 시간이 일정하지 않습니다.
그리고 복잡한 명령어 때문에 명령어 하나를 실행하는 데에 여러 쿨럭 주기를 필요로 합니다.
이는 명령어 파이프라인을 구현하는 데에 큰 걸림돌이 됩니다.
명령어 파이프라인 기법을 위한 이상적인 명령어는 다음 그림과 같이 각 단계에 소요되는 시간이 (가급적 1 클럭으로) 동일해야 합니다.
그래야 파이프라인이 마치 공장의 생산 라인처럼 결과를 내기 때문입니다.
하지만 CISC가 활용하는 명령어는 명령어 수행 시간이 길고 가지각색이기 때문에 파이프라인이 효율적으로 명령어를 처리할 수 없습니다.
한마디로 규격화되지 않은 명령어가 파이프라이닝을 어렵게 만든 셈입니다.
명령어 파이프라인이 제대로 동작하지 않는다는 것은 현대 CPU에서 아주 치명적인 약점입니다.
현대 CPU에서 명령어 파이프라인은 높은 성능을 내기 위해 절대 놓쳐서는 안 되는 핵심 기술이기 때문입니다.
게다가 CISC가 복잡하고 다양한 명령어를 활용할 수 있다고는 하지만, 사실 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.
1974년 IBM 연구소의 존 코크(John Cocke)는 CISC 명령어 집합 중 불과 20% 정도의 명령어가 사용된 전체 명령어의 80%가량을 차지한다는 것을 증명하기도 했습니다.
CISC 명령어 집합이 다양하고 복잡한 기능을 지원하지만 실제로는 자주 사용되는 명령어만 쓰였다는 것입니다.
정리하자면, CISC 명령어 집합은 복잡하고 다양한 기능을 제공하기에 적은 수의 명령으로 프로그램을 동작시키고 메모리를 절약할 수 있지만, 명령어의 규격화가 어려워 파이프라이닝이 어렵습니다.
그리고 대다수의 복잡한 명령어는 그 사용 빈도가 낮습니다.
이러한 이유로 CISC 기반 CPU는 성장에 한계가 있습니다.
RISC
CISC의 한계가 우리들에게 준 교훈은 크게 아래와 같습니다.
빠른 처리를 위해 명령어 파이프라인을 십분 활용해야 한다. 원활한 파이프라이닝을 위해 ‘명령어 길이와 수행 시간이 짧고 규격화’되어 있어야 한다.
어차피 자주 쓰이는 명령어만 줄곧 사용된다. 복잡한 기능을 지원하는 명령어를 추가하기보다는 ‘자주 쓰이는 기본적인 명령어를 작고 빠르게 만드는 것’이 중요하다.
이런 원칙 하에 등장한 것이 RISC입니다.
RISC(Reduced Instruction Set Computer) : 이름처럼 CISC에 비해 명령어의 종류가 적습니다. 그리고 CISC와는 달리 짧고 규격화된 명령어, 되도록 1클럭 내외로 실행되는 명령어를 지향합니다.
즉, 고정 길이 명령어를 활용합니다.
명령어가 규격화되어 있고, 하나의 명령어가 1클럭 내외로 실행되기 때문에 RISC 명령어 집합은 명령어 파이프라이닝에 최적화되어 있습니다.
그리고 RISC는 메모리에 직접 접근하는 명령어를 load, store 두 개로 제한할 만큼 메모리 접근을 단순화하고 최소화를 추구합니다.
그렇기 때문에 CISC보다 주소 지정 방식의 종류가 적은 경우가 많습니다.
이런 점에서 RISC를 load-store 구조라고 부르기도 합니다.
RISC는 메모리 접근을 단순화, 최소화하는 대신 레지스터를 적극적으로 활용합니다.
그렇기에 CISC보다 레지스터를 이용하는 연산이 많고, 일반적인 경우보다 범용 레지스터 개수도 더 많습니다.
다만 사용 가능한 명령어 개수가 CISC보다 적기 때문에 RISC는 CISC보다 많은 명령으로 프로그램을 작동시킵니다.
키워드로 정리하는 핵심 포인트
ISA는 CPU의 언어이자 하드웨어가 소프트웨어를 어떻게 이해할지에 대한 약속입니다.
CISC는 복잡하고 다양한 종류의 가변 길이 명령어 집합을 활용합니다.
RISC는 단순하고 적은 종류의 고정 길이 명령어 집합을 활용합니다.
-
-
-
-
💻[Operating System] 리눅스와 우분투의 차이점
리눅스와 우분투의 차이점.
“리눅스” 와 “우분투” 의 차이점을 이해하려면 먼저 리눅스가 무엇인지, 그리고 우분투가 어떻게 이와 관련되어 있는지를 알아야 합니다.
리눅스(Linux).
리눅스는 주로 운영 체제의 커널을 가리키는 용어입니다.
커널은 하드웨어와 소프트웨어 컴포넌트 사이에서 통신을 중재하고, 시스템 리소스를 관리하는 핵심 소프트웨어 컴포넌트입니다.
여기서 컴포넌트란 무엇일까요?
“컴포넌트” 란 일반적으로 더 큰 시스템의 일부로 기능하는 개별적인 부품이나 요소를 의미합니다
이 용어는 다양한 분야에서 사용되며, 각각의 맥락에 따라 다소 다른 의미를 가질 수 있습니다.
이 포스팅에서는 소프트웨어 개발에서의 컴포넌트만 설명하겠습니다.
소프트웨어 개발에서의 컴포넌트
소프트웨어 개발에서는 컴포넌트가 소프트웨어의 모듈이나 라이브러리 형태로 존재할 수 있습니다.
이런 컴포넌트들은 재사용 가능하며, 특정 기능을 수행하기 위해 독립적으로 개발되고 통합될 수 있습니다.
예를 들어, 웹 애플리케이션에서 로그인 모듈, 검색 엔진, 사용자 인터페이스 요소 등이 각각의 컴포넌트로 구성될 수 있습니다.
리눅스 커널은 오픈 소스이며, 1991년 리누스 토발즈에 의해 처음 개발되었습니다.
리눅스 커널 자체는 독립적으로 사용할 수 없으며, 보통 시스템 라이브러리, 유틸리티, 필수 프로그램 등과 함께 배포되어 전체 운영 체제의 기반을 형성합니다.
우분투(Ubuntu).
우분투는 리눅스 커널을 기반으로 한 리눅스 배포판 중 하나입니다.
이는 사용자 친화적인 인터페이스, 풍부한 소프트웨어 저장소, 정기적인 업데이트 및 지원을 제공하며, 개인용 컴퓨터, 서버, 클라우드 등 다양한 완경에서 사용할 수 있습니다.
우분투는 데비안(Debian) 리눅스 배포판을 기반으로 만들어졌으며, 쉽게 접근할 수 있고 설치 및 사용이 간편하다는 점에서 초보자에게 인기가 많습니다.
리눅스와 우분투의 주요 차이점.
1. 정의와 범위 : 리눅스는 커널의 이름이며, 우분투는 리눅스 커널을 포함한 전체 운영 체제의 한 배포판입니다.
2. 사용성 : 커널 자체는 기술적인 측면에서만 다루어지지만, 우분투는 끝 사용자를 대상으로 한 GUI와 사용자 친화적 도구를 제공합니다.
3. 목적 : 리눅스 커널은 다양한 운영 체제의 기반으로 사용됩니다. 반면, 우분투는 개인 사용자와 기업 환경 모두를 겨냥해 특정한 목적과 요구를 충족시키기 위해 개발되었습니다.
요약.
리눅스는 기술적인 컴포넌트(커널)를 지칭하고 우분투는 그 커널을 사용하여 만들어진 하나의 완성된 운영 체제 배포판 입니다.
-
💻[Operating System] 커널(kernel)이란?
커널(kernel)이란?
커널의 역할.
커널(kernel)은 컴퓨터 운영 체제의 핵심 구성 요소로서, 하드웨어와 소프트웨어 리소스 간의 통신을 중재하고 시스템의 모든 주요 기능을 관리하는 역할을 합니다.
커널의 기능.
커널의 기능은 매우 광범위하며, 그 중 몇 가지 주요 기능을 다음과 같이 설명할 수 있습니다.
1. 프로세스 관리.
커널은 시스템에서 실행되는 모든 프로세스(활성 프로그램)의 생성, 실행 및 종료를 관리합니다.
프로세스 관리는 프로세스 스케줄링, 상태 관리, 우선순위 할당 등을 포함합니다.
이를 통해 시스템 리소스가 효율적으로 활용되고, 여러 프로세스 간에 시스템 리소스를 공정하게 분배할 수 있습니다.
2. 메모리 관리.
커널은 시스템의 물리적 메모리(RAM)를 관리하고 각 프로그램에 필요한 메모리 공간을 할당 및 회수합니다.
메모리 관리 기능은 메모리 보호, 메모리 할당, 가상 메모리 시스템을 포함하여, 프로그램이 안정적으로 실행될 수 있도록 지원합니다.
3. 디바이스 드라이버와 I/O 관리.
커널은 하드웨어 디바이스와의 통신을 담당하는 디바이스 드라이버를 관리합니다.
입력 및 출력(I/O)장치(예: 키보드, 마우스, 디스플레이, 저장 장치 등)에 대한 접근과 데이터 전송을 총괄하며, 하드웨어의 올바른 동작을 보장합니다.
4. 파일 시스템 관리.
커널은 파일 시스템을 통해 데이터의 저장 및 검색을 관리합니다.
이는 파일 생성, 삭제, 읽기, 쓰기 등의 작업을 포함하며, 사용자와 응용 프로그램이 파일과 디렉토리를 효율적으로 사용할 수 있도록 합니다.
5. 보안 및 접근 제어.
커널은 시스템 보안을 유지하기 위해 사용자 권한 및 접근 제어를 관리합니다.
이를 통해 사용자의 권한에 따라 리소스 접근을 제한하고, 시스템의 안전성을 유지합니다.
6. 네트워킹
커널을 네트워크 통신을 관리하여, 컴퓨터가 네트워크를 통해 다른 시스템과 데이터를 교환할 수 있도록 지원합니다.
마무리.
커널은 일반적으로 시스템의 가장 낮은 수준에서 실행되며, 운영 체제의 나머지 부분과 사용자 애플리케이션으로부터 분리되어 있습니다.
이는 시스템의 핵심적인 부분을 안정적이고 효율적으로 관리할 수 있도록 하기 위함입니다.
커널의 설계와 구현은 운영 체제의 성능, 안정성 및 확장성에 직접적인 영향을 미칩니다.
-
📦[DataStructure] 삽입 정렬
삽입 정렬.
배열 구조를 어떻게 사용할 수 있는지 이해하는 가장 좋은 방법은 실제 알고리즘을 검토하는 것입니다.
삽입 정렬(insertion sort) 은 배열의 값을 정렬하는 알고리즘으로, 순서를 정할 수 있는 모든 유형의 값에서 작동합니다.
정수, 문자열, 심지어 유통기한에 따라 저장된 창고 안 커피까지 삽입 정렬로 정렬할 수 있습니다.
삽입 정렬은 배열의 일부를 정렬하고, 이 정렬된 범위를 전체 배열이 정렬될 때까지 확장합니다.
알고리즘은 정렬되지 않은 배열의 각 원소를 반복하면서 정렬된 부분의 올바른 위치로 이동합니다.
i의 반복을 시작하는 시점에 i-1 이하의 위치에 있는 원소는 모두 정렬되 있습니다.
알고리즘은 이제 인덱스 i에 있는 원소를 선택하고, 정렬된 접두사에서 이 원소의 올바른 위치를 찾아 나머지 원소를 뒤로 이동시켜서 선택한 원소가 들어갈 공간을 만든 수 삽입합니다.
그러면 정렬된 접두사가 하나 더 커지면서 0에서 i까지 모든 상자가 정렬된 상태가 됩니다.
처음에는 첫 번째 원소를 초기 정렬된 접두사로 선언하고 i = 1부터 반복을 시작할 수 있습니다.
커피 컬렉션을 신선도순으로 정렬하고 싶다고 합시다.
무엇보다 프리미엄 커피가 창고 깊숙이 박혀 있다 상해버리는 비극은 바람직하지 않습니다.
따라서 유통기한이 제일 짧게 남은 커피를 가장 앞쪽에 넣어서 쉽게 접근할 수 있게 해야합니다.
우선 커피백 하나를 정렬된 부분으로 선언하고, 이를 기준으로 정렬 범위를 설정함으로써 커피 정렬을 시작합니다.
그 다음에는 가장 앞쪽에서 두 번째 백부터 날짜를 비교해 정렬된 부분의 백보다 더 앞에 넣어야 할지를 판단합니다.
위치를 바꿀 필요가 있는 경우엔 순서를 바꾸고, 그렇지 않은 경우엔 자리를 유지합니다.
이제 자신 있게 맨 앞의 두 백이 정렬됐다고 말할 수 있습니다.
이렇게 부분적으로 정렬하는 과정을 마지막 백까지 진행하면서 위치를 바꾸는 작업을 반복하면, 커피 컬렉션을 완벽하게 정리할 수 있습니다.
아래 코드와 같이 중첩된 루프를 이용해 삽입 정렬을 구현할 수 있습니다.
InsertionSort(array: A):
Integer: N = length(A)
Integer: i = 1
WHILE i < N: // 1
Type: current = A[i]
Integer: j = i - 1
WHILE j >= 0 AND A[j] > current: // 2
A[j + 1] = A[j]
j = j - 1
A[j + 1] = current
i = i +1
바깥쪽 루프는 최초의 정렬되지 않은 원소인 인덱스 i가 1인 원소부터 시작하고 정렬되지 않은 범위에 있는 각 값을 반복합니다(1)
안쪽 루프는 인덱스 j를 사용해 정렬된 접두사의 원소를 맨 뒤에서부터 하나씩 반복합니다(2)
반복 각 단계에서 현재 값과 정렬된 접두사 안에 있는 인덱스 j의 값을 비교해 확인합니다.
j에 있는 원소가 더 크면 두 값의 순서가 잘못됐으므로 교환해야 합니다.
현재 값을 별도의 변수인 current에 저장했기 때문에 이전 상자에서 데이터를 직접 복사합니다.
즉, i번째와 j번째의 값을 완전히 교환할 필요가 없습니다.
내부 루프는 현재 값을 배열의 맨 앞에 밀어넣거나 현재 값보다 이전 값이 더 작을 때까지만(이 경우가 바로 현재 값이 정렬된 접두사의 올바른 위치에 있음을 나타냅니다.) 계속 진행합니다.
이제 내부 루프의 끝에서 현재 값을 올바른 위치에 쓰기만 하면 됩니다.
바깥쪽 루프는 다음 정렬되지 않은 값으로 진행합니다.
아래 그림은 알고리즘이 어떻게 동작하는지 시각화해 보여줍니다.
각 줄은 반복 시작 시 배열의 상태를 보여줍니다.
빨간색 상자는 현재 위치에 있는 원소를 나타내며, 화살표는 현재 위치의 원소를 삽입하면서 발생하는 이동을 나타냅니다.
삽입 정렬은 그렇게 효율적이지 않습니다.
배열에 원소를 삽입할 때, 상당 부분을 이동해야 할 수도 있습니다.
최악의 경우(worst-case), 알고리즘의 비용은 시퀀스 원소 수의 제곱에 비례합니다.
즉, 최악의 경우 리스트의 모든 원소마다 앞의 모든 원소를 이동해야합니다.
배열의 크기를 2배로 늘리면, 최악의 경우 비용이 4배 증가합니다.
그럼에도 불구하고 삽입 정렬은 배열이 어떻게 작동하는지 중요한 통찰을 제공합니다.
이 간단한 알고리즘은 인덱스를 사용해 원소레 직접 접근할 수 있어야 하며, 새 원소를 삽입할 때 값을 교환할 수 있어야 하며, 모든 원소를 반복(iteration)할 수 있어야 한다는 배열의 여러 특성을 보여줍니다.
-
📦[DataStructure] 문자열
문자열(String) 은 종종 특수한 종류의 배열로 생각할 수 있는, 순서가 지정된 문자의 리스트다.
문자열의 각 칸에는 문자, 숫자, 기호, 공뱁 또는 제한된 특수 기호 중 하나가 포함됩니다.
마지막 칸에 있는 특수 기호 /는 종종 문자열의 끝을 나타냅니다.
인덱스를 사용해 문자열의 문자에 직접 접근할 수 있습니다.
일부 프로그래밍 언어에서는 문자열을 그냥 문자 배열로 직접 구현합니다.
몇몇 다른 언어에서는 문자열이 객체일 수 있으며, 문자열 클래스는 문자를 담고 있는 배열이나 다른 자료 구조를 감싼 래퍼(wrapper) 클래스 역할을 합니다.
문자열 래퍼 크래스는 문자열의 크기를 동적으로 조정하거나 부분 문자열을 탐색하는 등 추가 기능을 제공합니다.
두 경우 모두 일반 배열과 유사한 구조가 문장열에 대한 작업에 어떤 영향을 미칠지 생각해보는 것이 유용합니다.
컴퓨터 화면에 문자열을 표시할 때는 문자열의 각 문자를 반복하면서 하나씩 문자를 표시합니다.
동등성(equality) 검사는 더 흥미롭습니다.
한 번의 연산으로 직접 비교할 수 있는 정수와 달리, 문자열은 각 문자를 반복하면서 비교해야 합니다.
두 문자열을 비교할 때는 서로 일치하지 않는 문자를 발견할 때까지 두 문자열에서 같은 위치에 존재하는 문자를 서로 비교합니다.
아래의 코드는 두 문자열의 동등성을 확인하는 알고리즘을 보여줍니다.
StringEqual(String: str1, String: str2):
IF length(str1) != length(str2):
return False
Integer: N = length(str1)
Integer: i = 0
WHILE i < N AND str1[i] == str2[i]:
i = i + 1
return i == N
알고리즘은 먼저 문자열의 크기를 비교합니다.
길이가 다르면 알고리즘은 해당 시점에 중지됩니다.
길이가 같으면 알고리즘은 각 위치를 반복하면서 해당 위치에 있는 두 문자를 비교합니다.
이때 두 문자가 서로 일치하지 않으면 루프를 중지할 수 있습니다.
문자열을 모두 비교했는데 불일치가 일어나지 않았다면 두 문자열을 같다고 선언할 수 있습니다.
아래의 그림은 이 알고리즘이 두 문자열에 대해 어떻게 작동하는지 보여줍니다. =는 비교할 때 서로 일치한 문자 쌍을 나타냅니다.
X는 최초 불일치로 인해 검사가 종료된 문자쌍을 나타냅니다.
문자열 비교에서 최악의 경우 계산 비용은 문자열의 길잉 비례해 증가합니다.
두 작은 문자열을 비교하는 작업에서는 무시할 수 있지만, 두 긴 문자열을 비교하는 작업에서는 시간이 오래 걸릴 수 있습니다.
예를 들어, 어떤 책의 1판과 2판을 처음부터 한 글자씩 비교하면서 두 책의 본문 문자 배열의 차이를 찾는 지겨운 과정을 상상해볼 수 있습니다.
가장 좋은 경우에는 초기에 일치하지 않는 부분을 찾을 수 있지만, 최악의 경우에는 책의 대부분을 검사해야 합니다.
많은 프로그래밍 언어, 예를 들어 파이썬과 같은 언어는 직접 비교할 수 있는 문자열 클래스를 제공합니다.
따라서 위 코드와 같은 비교 코드를 직접 구현할 필요가 없습니다.
그러나 간단한 비교 함수의 뒤에는 모든 문자를 반복하는 루프가 있습니다.
이 중요한 세부 사항을 이해하지 않으면 문자열 비교 비용을 과소평가할 수 있습니다.
-
📦[DataStructure] 배열
배열.
일반적으로 배열(array) 은 관련된 다수의 값을 저장할 때 사용합니다.
예를 들어, 1년간 매일 마신 커피의 양을 추적하고 싶다고 합시다.
이때 개별 변수(AmountDay1, AmountDay2, AmountDay3 등)를 365개 만들어서 저장할 수 있겠지만, 이 방식은 입력하기도 귀찮고 데이터를 어떤 구조로도 사용할 수 없습니다.
AmountDay2는 단지 텍스트 꼬리표일 뿐이며, AmountDay2 전날의 정보를 AmountDay1이 저장하고 AmountDay2 다음 날의 정보를 AmountDay3가 저장한다는 사실을 프로그램이 알 수 없습니다.
개발자만 이 정보를 알고 있습니다.
배열은 여러 값을 연속적으로 인데스(Index) 가 부여된 상자에 저장하는 간단한 메커니즘을 제공합니다.
아래의 그림처럼 배열은 사실 개별 변수들을 한 줄로 세워둔 것이며, 컴퓨터 메모리에 존재하는 같은 크기의 상자들이 연속적으로 배치된 블록입니다.
개별 변수처럼 배열도 어떤 메모리 덩어리를 차지하며 임의의 다른 정보와 인접할 수 있습니다.
배열의 각 상자에는 숫자, 문자, 포인터 또는 다른(크기가 정해져 있는) 자료 구조와 같은 타입의 값을 저장할 수 있습니다.
일상생활에서도 배열을 매우 많이 사용합니다.
예를 들어, 고등학교 복도에 늘어선 사물함은 학생들의 책과 외투를 저장하는 물리적인 배열입니다.
우리는 개별 사물함을 열어 내부 공간에 쉽게 접근할 수 있습니다.
배열의 구조는 위치(또는 인덱스)를 지정하여 배열 내 개별 값, 즉 원소(element) 에 접근할 수 있게 해줍니다.
배열 내 상자들은 컴퓨터 메모리에서 서로 인접해 있으므로, 첫 번째 원소로부터 오프셋(offset)을 계산해서 해당하는 위치의 메모리를 읽는 방식으로 각 상자에 쉽게 접근할 수 있습니다.
이는 접근하려는 상자의 위치와 관계없이 덧셈 한 번과 메모리 접근만 필요하다는 뜻입니다.
이러한 구조는 우리의 일일 커피 섭취량을 추적하는 것과 같이 순서가 있는 항목을 저장할 때 특히 편리합니다.
형식적으로 배열 A에서 인덱스 i에 있는 값을 A[i]로 참조합니다.
사물함 예제에서 인덱스는 사물함 앞에 표시된 숫자에 해당합니다.
대부분의 프로그래밍 언어는 0부터 시작하는(zero based) 인덱스를 사용합니다.
이 말은 아래의 그림처럼 배열의 첫 번째 값은 인덱스 0, 두 번째 값은 인덱스 1, …에 위치한다는 뜻입니다.
아래 그림은 컴퓨터 메모리 안 배열 모습을 보여줍니다.
여기서 흰 칸이 배열 원소에 해당합니다.
0을 기준으로 인덱싱하면 메모리 내에서 배열의 시작점부터 오프셋을 사용해 위치를 계산할 때 편리합니다.
i번째 원소의 위치는 다음과 같이 계산할 수 있습니다.
위치(인덱스 i의 원소) = 위치(배열 시작) + 각 원소의 크기 x i
인덱스 0의 위치는 배열 시작점과 같습니다.
예를 들어, 위 그림에서 배열 A의 다섯 번째 원소는 A[4]이며 그림 1-4를 찾아보면 그 위치에는 9라는 값이 들어 있습니다.
노트
인덱스를 1부터 시작하는 것도 가능하며, 일부 프로그래밍 언어는 이 규칙을 따릅니다.
1을 기준으로 인덱싱하는 경우 상자의 주소를 계산하는 식은 다음과 같습니다.
위치(인덱스 i의 원소) = 위치(배열 시작) + 각 원소의 크기 x (i-1)
대부분의 프로그래밍 언어에서는 배열 이름과 인덱스를 조합해 값을 가져오거나 설정합니다.
예를 들어, 다음과 같이 인덱스가 5인 상자의 값을 16으로 설정할 수 있습니다.
A[5] = 16
커피 추적 예제에서 하루 동안 섭취한 커피 컵 수를 저장하기 위해 Amount라는 배열을 정의하고, 해당 수량을 Amount[0] 부터 Amount[364]까지 저장할 수 있습니다.
배열을 사용하면 단 하나의 이름으로 365개 다른 값에 순서대로 접근 할 수 있는데, 이름은 비슷하지만 서로 독립적인 변수들을 연속적으로 위치시켰던 것을 수학적인 오프셋으로 전환한 것입니다.
이 개념의 장점을 이해하려면 학교 사물함을 생각하면 됩니다.
개별 사물함을 ‘제레미의 사물함’이나 ‘K로 시작하는 세 번째 학생의 사물함’처럼 이름 붙이면 빠르게 찾기가 거의 불가능합니다.
이런 방식을 사용하면 그냥 인덱스를 사용하는 경우와 달리 모든 사물함에 붙은 꼬리표를 일일이 찾아봐야 합니다.
하지만 배열 인덱스를 사용하면 학생들은 오프셋을 사용해 사물함이 어디 있는지 결정하고 직접 해당 사물함에 접근할 수 있습니다.
종종 배열을 전체 자료 구조로 시각화하고 논의하지만, 각 상자가 개별 변수처럼 작동한다는 사실을 기억하는 것이 중요합니다.
배열을 전체적으로 바꾸려면 모든 상자를 하나하나 바꿔야 합니다.
예를 들어, 원소를 한 칸 앞으로 이동시키고 싶으면 아래 그림처럼 해야 합니다.
배열은 책장에 꽂혀 있는 책들과 다릅니다.
‘커피 애호가를 위한 최고의 공정 무역 커피 가이드’를 끼워넣기 위해 책 컬렉션 전체를 밀어낼 수 있지만, 배열을 그렇지 않습니다.
배열은 오히려 일렬로 늘어선 가게와 같습니다.
서점과 미용실 사이에 커피숍을 끼어넣을 수 없습니다.
커피숍 공간을 확보하려면 인접한 건물로 서점(또는 미용실)을 이전해서 기존 공간을 비우는 방식으로 가게를 하나씩 옮겨야만 합니다.
실제로 배열에서 단순히 두 값을 교환하고 싶은 경우에도 값들을 미묘하게 조정해야 합니다.
예를 들어, 어떤 인덱스 i와 j에 있는 두 값을 교환하려면 먼저 둘 중 하나를 임시 변수에 할당해야 합니다.
Temp = A[i]
A[i] = A[j]
A[j] = Temp
그렇지 않으면 어떤 한 상자 안 값을 덮어쓰게 되어 두 상자가 동일한 값을 가지게 됩니다.
마찬가지로 커피숍과 서점의 위치를 바꾸려고 한다면, 먼저 서점의 가구와 물품 등을 비어 있는 세 번째 임시 위치로 커피숍의 것들을 넣을 수 있는 공간을 확보해야 합니다.
그 후 커피숍을 옮길 수 있고, 서점의 가구와 물품 등을 세 번째 임시 위치에서 커피숍의 이전 위치로 옮길 수 있습니다.
-
-
📦[DataStructure] 변수
변수.
개변 데이터 조각을 종종 변수(variable)에 저장하곤 합니다.
변수(variable) : 컴퓨터 메모리 내 데이터 위치(또는 주소)를 표현하는 이름입니다.
프로그램 실행 중 변경되는 정보를 추적할 수 있게 합니다.
예를 들어 For 루프를 몇 번지나갔는지 세어야 할 경우, 게임에서 플레이어의 점수를 추적해야하는 경우 등
변수가 없으면 프로그램의 내부 상태를 추적, 평가(evaluate), 변경(update)할 수 없습니다.
변수를 생성하면 시스템이 그것을 자동으로 할당하고 위치를 지정합니다.
그리고 나서 원하는 변수 이름을 사용해 자유롭게 해당 위치에 데이터를 쓰고, 데이터를 쓸 때 사용한 변수 이름을 사용해 저장된 데이터를 읽을 수 있습니다.
변수 이름만 알고 있다면 데이터의 메모리 위치를 알 필요가 없습니다.
컴퓨터 메모리를 여러 상자가 일렬로 늘어선 것처럼 생각할 수도 있습니다.
각 변수는 저장한 데이터의 크기에 따라 하나 이상의 인접한 상자를 차지합니다.
아래 그림은 Level, Score, AveScore라는 세 변수를 보여줍니다.
여기서 평균 점수(AveScore)는 메모리 상자를 두 개 사용하는 부동 소수점 수(floating point number, 소수점이 있는 숫자)입니다.
어떤 측면에서 변수는 종이 문서를 담는 폴더에 붙은 종이 라벨과 비슷합니다.
아래 그림처럼 라벨을 붙인 후에는 폴더의 순서나 정확한 위치를 기억할 필요가 없습니다.
그 이유는 라벨로 폴더를 찾으면 되기 때문입니다.
이때 충분한 정보가 포함된 이름을 사용하는 것이 중요합니다.
만약에 파일 캐비닛에 할 일, 중요한 일, 다른 할 일, 그 밖의 일과 같이 이름이 겹치는(이를 오버로드(overload)라고 말합니다) 폴더가 많을 경우 내용을 파악하기 어렵습니다.
마찬가지로, 변수의 이름이 모호하면 변수가 어떤 값을 나타내는지 추측하기 어려워집니다.
많은 프로그래밍 언어에서 변수는 정수(integer), 부동 소수점 값(float), 불린 값(Boolean) 등과 같이 저장된 데이터의 타입과 연관이 있습니다.
타입은 변수가 얼마나 많은 메모리를 차지하고 메모리에 저장된 내용을 어떻게 사용해야 하는지를 프로그램에 알려줍니다.
예를 들어, 불린 변수는 제한된 범위의 값(즉, 참과 거짓)만 저장하며 적은 양의 메모리만 사용하는 경우가 많습니다.
반면, 2배 정밀도(double-precision) 부동 소수점 수는 훨씬 더 크고 정확한 숫자를 저장하므로 여러 상자를 사용합니다.
타입을 정의하는 문법이나 타입을 명시적으로 정의해야만 하는지 여부는 프로그래밍 언어마다 다릅니다.
아래 예제를 봐봅시다.
예제에서는 변수를 명시할 때 언어와 무관한 <타입>: <변수이름>이라는 의사 코드(pseudocode) 형식을 사용합니다.
Integer: coffee_count = 5
Float: percentage_words_spelled_correctly = 21.0
Boolean: had_enough_coffee = False
가씀 Type이라는 타입이 지정된 변수도 있습니다.
이 타입은 어떻게 구현하는지에 따라 다양한 타입이 될 수 있다는 사실을 나타냅니다.
대부분의 프로그래밍 언어에서 일반적으로 사용되는 구문을 사용해 변수를 다룰 것입니다.
예를 들어, 변수에 값을 대입할 때는 =을 사용합니다.
coffee_count = coffee_count + 1
-
-
☕️[Java] System 클래스
System 클래스
System 클래스는 시스템과 관련된 기본 기능들을 제공합니다.
package lang.system;
import java.util.Arrays;
public class SystemMain {
public static void main(String[] args) {
// 현재 시간(밀리초)를 가져옵니다.
long currentTimeMillis = System.currentTimeMillis();
System.out.println("currentTimeMillis = " + currentTimeMillis);
// 현재 시간(나노초)를 가져옵니다.
long currentTimeNano = System.nanoTime();
System.out.println("currentTimeNano = " + currentTimeNano);
// 환경 변수를 읽습니다.
System.out.println("getenv = " + System.getenv());
// 시스템 속성을 읽습니다.
System.out.println("properties = " + System.getProperties());
System.out.println("Java version = " + System.getProperty("java.version"));
// 배열을 고속으로 복사합니다.
char[] originalArray = { 'h', 'e', 'l', 'l', 'o' };
char[] copiedArray = new char[5];
System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length);
// 배열 출력
System.out.println("copiedArray = " + copiedArray);
System.out.println("Arrays.toString = " + Arrays.toString(copiedArray));
// 프로그램 종료
System.exit(0);
}
}
실행 결과
currentTimeMillis = 1713485140558
currentTimeNano = 694481339313708
getenv = {HOMEBREW_PREFIX=/opt/homebrew, MANPATH=/Users/kobe/.nvm/versions/node/v20.10.0/share/man:/opt/local/share/man:/opt/homebrew/share/man::, COMMAND_MODE=unix2003, INFOPATH=/opt/homebrew/share/info:...
properties = {java.specification.version=21, sun.jnu.encoding=UTF-8, java.class.path=/Users/kobe/Desktop/practiceJavaMidPart1/java-mid1/out/production/java-mid1, java.vm.vendor=Oracle Corporation, sun.arch.data.model=64, java.vendor....
Java version = 21.0.2
copiedArray = [C@77459877
Arrays.toString = [h, e, l, l, o]
Process finished with exit code 0
표준 입력, 출력, 오류 스트림: System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타냅니다.
시간 측정: System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공합니다.
환경 변수: System.getProperties()를 사용해 현재 시스템 속성을 얻거나 System.getProperty(Stringkey)로 특정 속성을 얻을 수 있습니다. 시스템 속성은 자바에서 사용하는 설정 값입니다.
시스템 종료: System.exit(int status) 메서드는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달합니다.
상태 코드0 : 정상 종료
상태 코드 0이 아님: 오류나 예외적인 종료
배열 고속 복사: System.arraycopy는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용합니다. 직접 반복문을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공합니다.
-
☕️[Java] Class 클래스
Class 클래스.
자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다.
Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다.
Class 클래스의 주요 기능은 다음과 같습니다.
타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다.
리플렉션: 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다.
동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다.
애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다.
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다.
다음 코드를 실행해봅시다.
package lang.clazz;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
Class clazz = String.class; // 1. 클래스에서 조회
//Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
//Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
// 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interfaces) {
System.out.println("Interface: = " + i.getName());
}
}
}
class vs clazz - class는 자바의 예약어입니다. 따라서 패키지명, 변수명으로 사용할 수 없습니다.
이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용합니다.
clazz는 class와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있습니다.
주의!
main() 옆에 throws Exception이 추가된 부분에 주의합시다. 이 코드가 없으면 컴파일 오류가 발생합니다.
실행 결과
field = class [B value
...
method = byte[] java.lang.String.value()
method = public boolean java.lang.String.equals(java.lang.Object)
...
Superclass: java.lang.Object
Interface: = java.io.Serializable
Interface: = java.lang.Comparable
...
Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다.
Class clazz = String.class; // 1. 클래스에서 조회
Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
Class clazz = Clazz.forName("java.lang.String"); // 3. 문자열로 조회
Class 클래스의 주요 기능
getDeclaredFields(): 클래스의 모든 필드를 조회합니다.
getDeclaredMethods(): 클래스의 모든 메서드를 조회합니다.
getSuperclass(): 클래스의 부모 클래스를 조회합니다.
getInterface(): 클래스의 인터페이스들을 조회합니다.
실행 결과를 보면 String 클래스의 다양한 정보를 확인할 수 있습니다.
클래스 생성하기
Class 클래스에는 클래스의 모든 정보가 들어있습니다. 이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다.
여기서는 간단하게 인스턴스를 생성해봅시다.
package lang.clazz;
public class ClassCreatMain {
public static void main(String[] args) throws Exception {
//Class helloClass = Hello.class;
Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
실행 결과
result = hello!
getDeclaredConstructor().newInstance()
getDeclaredConstructor() : 생성자를 선택합니다.
newInstance() : 선택된 생성자를 기반으로 인스턴스를 생성합니다.
리플랙션 - reflection
Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다.
이런 작업을 리플렉션이라고 합니다.
추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수도 있습니다.
최신 프레임워크들은 이런 기능을 적극 활용합니다.
지금은 Class가 뭔지, 그리고 대략 어떤 기능들을 제공하는지만 알아두면 충분합니다.
지금은 리플랙션을 학습하는 것 보다 훨씬 더 중요한 기본기들을 학습해야 합니다.
-
💾 [CS] 명령어 병렬 처리 기법
명령어 병렬 처리 기법
명령어 병령 처리 기법(ILP: Instruction-Level Parallelism): 명령어를 동시에 처리하여 CPU를 한시도 쉬지 않고 작동시키는 기법.
대표적인 명령어 병렬 처리 기법
명령어 파이프 라이닝
슈퍼스칼라
비순차적 명령어 처리
명령어 파이프라인
명령어 파이프라인을 이해하려면 하나의 명령어가 처리되는 전체 과정을 비슷한 시간 간격으로 나누어 보아야 합니다.
명령어 처리 과정을 클럭 단위로 나누어 보면 일반적으로 다음과 같이 나눌 수 있습니다.
명령어 인출(Instruction Fetch)
명령어 해석(Instruction Decode)
명령어 실행(Execute Instruction)
결과 저장(Write Back)
참고: 이 단계가 정답은 아닙니다.
전공서에 따라 명령어 인출 -> 명령어 실행으로 나누기도 하고,
명령어 인출 -> 명령어 해석 -> 명령어 실행 -> 메모리 접근 -> 결과 저장으로 나누기도 합니다.
여기서 중요한 점은 같은 단계가 겹치지만 않는다면 CPU가 ‘각 단계를 동시에 실행할 수 있다’는 것입니다.
예를 들어 CPU는 한 명령어를 ‘인출’하는 동안에 다른 명령어를 ‘실행’할 수 있고, 한 명령어가 ‘실행’되는 동안에 연산 결과를 ‘저장’할 수 있습니다.
이를 그림으로 표현하면 다음과 같습니다.
t1에는 명령어 1, 2를 동시에 처리할 수 있고 t2에는 명령어 1,2,3을 동시에 처리할 수 있습니다.
이처럼 명령어를 겹처서 수행하면 명령어를 하나하나 실행하는 것보다 훨씬 더 효율적으로 처리할 수 있습니다.
이처럼 마치 공장 생산 라인과 같이 명령어들을 “명령어 파이프라인(instruction pipeline)” 에 넣고 동시에 처리하는 기법을 “명령어 파이프라이닝(instruction pipelining)” 이라고 합니다.
명령어 파이프라인을 사용하지 않고 모든 명령어를 순차적으로만 처리한다면 아래와 같이 처리했을것입니다.
한눈에 봐도 명령어 파이프라이닝을 이용하는 것이 더 효율적임을 알 수 있습니다.
파이프라이닝이 높은 성능을 가져오기는 하지만, 특정 상황에서는 성능 향상에 실패하는 경우도 있습니다.
이러한 상황을 파이프라인 위험(pipeline hazard) 이라고 부릅니다.
파이프라인 위험에는 크게 3가지가 있습니다.
데이터 위험
제어 위험
구조적 위험
데이터 위험
데이터 위험(data hazard) 은 명령어 간 ‘데이터 의존성’에 의해 발생합니다.
모든 명령어를 동시에 처리할 수는 없습니다.
어떤 명령어는 이전 명령어를 끝까지 실행해야만 비로소 실행할 수 있는 경우가 있습니다.
예를 들어 아래 두 명령어를 봅시다.
편의상 레지스터 이름을 R1, R2, R3, R4, R5라 하고 ‘왼쪽 레지스터에 오른쪽 결과를 저장하라’는 기호는 <- 기호로 표기하겠습니다.
명령어 1: R1 <- R2 + R3 // R2 레지스터 값과 R3 레지스터 값을 더한 값을 R1 레지스터에 저장
명령어 2: R4 <- R1 + R5 // R1 레지스터 값과 R5 레지스터 값을 더한 값을 R4 레지스터에 저장
위의 경우 명령어 1을 수행해야만 명령어 2를 수행할 수 있습니다.
즉, R1에 R2 + R3 결괏값이 저장되어야 명령어 2를 수행할 수 있습니다.
만약 명령어 1 실행이 끝나기 전에 명령어 2를 인출하면 R1에 R2 + R3 결괏값이 저장되기 전에 R1 값을 읽어 들이므로 원치 않은 R1 값으로 명령어 2를 수행합니다.
따라서 명령어 2는 명령어 1의 데이터에 의존적입니다.
이처럼 데이터 의존적인 두 명령어를 무작정 동시에 실행하려고 하면 파이프라인이 제대호 작동하지 않는 것을 ‘데이터 위험’이라고 합니다.
제어 위험
제어 위험(control hazard) 은 주로 분기 등으로 인한 ‘프로그램 카운터의 갑작스러운 변화’에 의해 발생합니다.
기본적으로 프로그램 카운터는 ‘현재 실행 중인 명령어의 다음 주소’로 갱신됩니다.
하지만 프로그램 실행 흐름이 바뀌어 명령어가 실행되면서 프로그램 카운터 값에 갑작스러운 변화가 생긴다면 명령어 파이프라인에 미리 가지고 와서 처리 중이었던 명령어들은 아무 쓸모가 없어집니다.
이를 ‘제어 위험’이라고 합니다.
참고: 참고로 이를 위해 사용하는 기술 중 하나가 분기 예측(branch prediction) 입니다.
분기 예측은 프로그램이 어디로 분기할지 미리 예측한 후 그 주소를 인출하는 기술입니다.
구조적 위험
구조적 위험(structural hazard) 은 명령어들을 겹쳐 실행하는 과정에서 서로 다른 명령어가 동시에 ALU, 레지스터 등과 같은 CPU 부품을 사용하려고 할 때 발생합니다.
구조적 위험은 자원 위험(resource hazard) 이라고도 부릅니다.
슈퍼스칼라
파이프라이닝은 단일 파이프라인으로도 구현이 가능하지만, 오늘날 대부분의 CPU에서는 여러 개의 파이프라인을 이용합니다.
이처럼 CPU 내부에 여러 개의 명령어 파이프라인을 포함한 구조를 슈퍼스칼라(superscalar) 라고 합니다.
명령어 파이프라인을 하나만 두는 것이 마치 공장 생산 라인을 한 개 두는 것과 같다면, 슈퍼스칼라는 공장 생산 라인을 여러 개 두는 것과 같습니다.
슈퍼스칼라 구조로 명령어 처리가 가능한 CPU를 슈퍼스칼라 프로세서 또는 슈퍼스칼라 CPU라고 합니다.
슈퍼스칼라 프로세서는 매 클럭 주기마다 동시에 여러 명령어를 인출할 수도, 실행할 수도 있어야 합니다.
가령 멀티스레드 프로세서는 한 번에 여러 명령어를 인출하고, 해석하고, 실행할 수 있기 때문에 슈퍼스칼라 구조를 사용할 수 있습니다.
슈퍼스칼라 프로세서는 이론적으로 파이프라인 개수에 비례하여 프로그램 처리 속도가 빨라집니다.
하지만 파이프라인 위험 등의 예상치 못한 문제가 있어 실제로는 반드시 파이프라인 개수에 비례하여 빨라지지는 않습니다.
이 때문에 슈퍼스칼라 방식을 차용한 CPU는 파이프라인 위험을 방지하기 위해 고도로 설계되어야 합니다.
여러 개의 파이프라인을 이용하면 하나의 파이프라인을 사용할 때 보다 데이터 위험, 제어 위험, 자원 위험을 피하기가 더욱 까다롭기 때문입니다.
비순차적 명령어 처리
비순차적 명령어 처리(OoOE: Out-of-order execution): 보통 OoOE로 줄여 부릅니다. 이 기법은 많은 전공서에서 다루지 않지만, 오늘날 CPU 성능 향상에 크게 기여한 기법이자 대부분의 CPU가 차용하는 기법입니다.
비순차적 명령어 처리 기법은 이름에서도 알 수 있듯 명령어들을 순차적으로 실행하지 않는 기법입니다. 명령어의 ‘합법적인 새치기’라고 볼 수 있습니다.
지금까지 설명했던 명령어 파이프라이닝, 슈퍼스칼라 기법은 모두 여러 명령어의 순차적인 처리를 상정한 방법이었습니다.
프로그램을 위에서 아래로 차례차례 실행하는 방식이었습니다.
하지만 파이프 라인 위험과 같은 예상치 못한 문제들로 인해 이따금씩 명령어는 곧바로 처리되지 못하기도 합니다.
만약 모든 명령어를 순차적으로만 처리한다면 이런 예상치 못한 상황에서 명령어 파이프라인은 멈춰버리게 됩니다.
예를 들어 아래와 같은 명령어들로 이루어진 소스 코드가 있다고 해봅시다.
편의상 ‘메모리 N번지’는 M(N)으로. ‘메모리 N번지에 M을 저장하라’는 M(N) <- M으로 표기하겠습니다.
1. M(100) <- 1
2. M(101) <- 2
3. M(103) <- M(100) + M(101)
4. M(150) <- 1
5. M(151) <- 2
6. M(152) <- 3
여기서 주목해야 할 점은 3번 명령어를 실행하기 위해서는 M(100) 값은 물론 M(101) 값이 결정되어야 하기에 1번과 2번 명령어 실행이 끝날 때까지 기다려야 한다는 점입니다.
이 명령어들을 순차적으로 실행되는 CPU로 실행하면 다음과 같습니다.
2번 명령어 실행이 끝날 때까지 3, 4, 5, 6번 명령어들은 기다립니다.
앞의 코드를 이루는 명령어들 중에 서로 데이터 의존성이 전혀 없는, 순서를 바꿔 처리해도 수행 결과에 영향을 미치지 않는 명령어들이 있습니다.
가령 3번은 다음과 같이 뒤의 명령어와 순서를 바꾸어 실행해도 크게 문제될 것이 없습니다.
이렇게 순서를 바꿔 실행하면 아래와 같이 수행됩니다.
순차적으로 명령어를 처리할 때보다 더 효율적입니다.
이렇게 명령어를 순차적으로만 실행하지 않고 순서를 바꿔 실행해도 무방한 명령어를 먼저 실행하여 명령어 파이프라인이 멈추는 것을 방지하는 기법을 비순차적 명령어 처리 기법 이라고 합니다.
하지만 아무 명령어나 순서를 바꿔서 수행할 수는 없습니다.
예를 들어서 다음 예시를 봅시다.
1. M(100) <- 1
2. M(101) <- 2
3. M(102) <- M(100) + M(101)
4. M(103) <- M(102) + M(101)
5. M(104) <- M(100)
위 코드에서 3번 명령어와 1번 명령어의 순서를 바꿀 수는 없습니다.
3번 명령어를 수행하려면 반드시 M(100) 값이 결정되어야 하기 때문입니다.
마찬가지로 4번 명령어와 1번 명령어는 순서를 바꿀 수 없습니다.
1번 명령어를 토대로 3번 명령어가 수행되고, 3번 명령어를 토대로 4번이 수행되기 때문입니다.
하지만 위 코드에서 4번 명령어와 5번 명령어는 순서를 바꾸어 실행할 수 있습니다.
다시 말해 이 두 명령어는 어떤 의존성도 없기에 순서를 바꿔도 전체 프로그램의 실행 흐름에는 영향이 없습니다.
이처럼 비순차적 명령어 처리가 가능한 CPU는 명령어들이 어떤 명령어와 데이터 의존성을 가지고 있는지, 순서를 바꿔 실행할 수 있는 명령어에는 어떤 것들이 있는지를 판단할 수 있어야 합니다.
키워드로 정리하는 핵심 포인트
명령어 파이프라이닝은 동시에 여러 개의 명령어를 겹쳐 실행하는 기법입니다.
슈퍼 스칼라는 여러 개의 명령어 파이프라인을 두는 기법입니다.
비순차적 명령어 처리 기법은 파이프라인의 중단을 방지하기 위해 명령어를 순차적으로 처리하지 않는 기법입니다.
-
💾 [CS] 빠른 CPU를 위한 설계 기법
빠른 CPU를 위한 설계 기법.
클럭
조금이라도 더 빠른 CPU를 만들려면 어떻게 CPU를 설계해야 할까요?
이전에 학습한 내용을 상기해봅시다.
컴퓨터 부품들은 ‘클럭 신호’에 맞춰 일사분란하게 움직인다.
CPU는 ‘명령어 사이클’이라는 정해진 흐름에 맞춰 명령어들을 실행한다.
클럭 신호가 빠르게 반복되면 CPU를 비롯한 컴퓨터 부품들은 그만큼 빠른 박자에 맞춰 움직일 것 입니다.
즉, 클럭 속도가 높아지면 CPU는 명령어 사이클을 더 빠르게 반복할 것이고, 다른 부품들도 그에 발맞춰 더 빠르게 작동할 것입니다.
실제로 클럭 속도가 높은 CPU는 일반적으로 성능이 좋습니다.
그래서 클럭 속도는 CPU 속도 단위로 간주되기도 합니다.
클럭 속도: 헤르츠(Hz) 단위로 측정합니다. 이는 1초에 클럭이 몇 번 반복되는지를 나타냅니다.
가령 클럭이 ‘똑-딱-‘하고 1초에 한 번 반복되면 CPU 클럭 속도는 1Hz인 것이고, 클럭이 1초에 100번 반복되면 CPU 클럭 속도는 100Hz인 셈입니다.
실제 CPU 클럭 속도는 위 사진 속 CPU를 보면 알 수 있습니다.
위 사진 속 CPU를 보면 기본 속도(Base)는 2.5GHz, 최대 속도(Max)는 4.9GHz라는 것을 알 수 있습니다.
이는 1초에 클럭이 기본적으로 25억(2.5 x 10⁹)번 반복된다는 것을 나타냅니다.
참고: 1GHz는 1,000,000,000(10⁹)Hz입니다.
“클럭 속도는 일정하지 않다.”
‘클럭’이라는 단어만 보고 시계를 떠올려 클럭 속도가 매번 일정하게 유지된다고 생각할 수도 있지만, 실제로는 그렇지 않습니다.
CPU 사진을 다시 보면 기본 클럭 속도(Base)와 최대 속도(Max)로 나위어 있습니다.
이처럼 CPU는 계속 일정한 클럭 속도를 유지하기보다는 고성능을 요하는 순간에는 순간적으로 쿨럭 속도를 높이고, 그렇지 않을 때는 유연하게 쿨럭 속도를 낮추기도 합니다.
최대 클럭 속도를 강제로 더 끌어올릴 수도 있는데, 이런 기법을 오버클럭킹(overclocking) 이라고 합니다.
클럭 속도를 무지막지하게 높이면 CPU는 무작정 빨라지지 않습니다.
그래픽이 많이 요구되는 게임이나 영상 편집과 같이 CPU에 무리가 가는 작업을 장시간 하면 컴퓨터가 뜨겁게 달아오르는 것을 경험해 본 적이 있을 겁니다.
클럭 속도를 무작정 높이면 이러한 발열 문제가 더 심각해집니다.
이처럼 클럭 속도를 높이는 것은 분명 CPU를 빠르게 만들지만, 클럭 속도만으로 CPU의 성늘을 올리는 것에는 한계가 있습니다.
코어와 멀티 코어
클럭 속도를 높이는 방법 외에 CPU의 성능을 높이는 방법에는 대표적으로 CPU의 코어와 스레드 수를 늘리는 방법이 있습니다.
먼저 코어를 늘리는 방법을 알아봅시다.
코어를 이해하려면 현대적인 관점에서 CPU라는 용어를 재해석해야 합니다.
앞서 CPU를 ‘명령어를 실행하는 부품’이라고 소개했습니다.
많은 전공 서적들의 전통적인 관점에서 ‘명령어를 실행하는 부품’은 원칙적으로 하나만 존재했습니다.
하지만 오늘날 CPU는 많은 기술적 발전을 거듭하였고, 그 결과 CPU 내부에는 ‘명령어를 실행하는 부품’을 얼마든지 만들 수 있게 되었습니다.
우리가 지금까지 CPU의 정의로 알고 있었던 ‘명령어를 실행하는 부품’은 오늘날 코어(core) 라는 용어로 사용됩니다.
다시 말해, 오늘날의 CPU는 단순히 ‘명령어를 실행하는 부품’에서 ‘명령어를 실행하는 부품을 여러 개 포함하는 부품’으로 명칭의 범위가 확장 되었습니다.
예를 들어 8코어(Core) CPU는 ‘명령어를 실행하는 부품’을 여덟 개 포함하고 있다고 보면 됩니다.
코어를 여러 개 포함하고 있는 CPU를 멀티코어(multi-core) CPU 또는 멀티코어 프로세서라고 부릅니다.
이는 CPU 내에 명령어를 처리하는 일꾼이 여러 명 있는 것과 같습니다.
당연히 멀티코어의 처리 속도는 단일코어보다 더 빠릅니다.
다령 클럭 속도가 2.4GHz인 단일 코어 CPU와 클럭 속도가 1.9GHz인 멀티코어 CPU를 비교하면 일반적으로 후자의 성능이 더 좋습니다.
CPU 종류는 CPU 안에 코어가 몇 개 포함되어 있는지에 따라 아래 표와 같이 싱글코어, 듀얼코어, 트리플코어 등으로 나뉩니다.
코어를 늘릴수록 연산 처리 속도도 빨라질까요?
CPU의 연산 속도가 꼭 코어 수에 비례하여 증가하지는 않습니다.
코어마다 처리할 연산이 적절히 분배되지 않는다면 코어 수에 비례하여 연산 속도가 증가하지 않습니다.
또한 처리하고자 하는 작업량보다 코어 수가 지나치게 많아도 성능에는 크게 영향이 없습니다.
중요한 것은 코어마다 처리할 명령어들을 얼마나 적절하게 분배하느냐이고 그에 따라서 연산 속도는 크게 달라집니다.
스레드와 멀티스레드
스레드(thread): 사전적 의미는 ‘실행 흐름의 단위’입니다.
하지만 이 정의를 활자 그대로 받아들이지 말고 더욱 엄밀하게 이해해야 합니다.
CPU에서 사용되는 스레드와 프로그래밍에서 사용되는 스레드는 용례가 다르기 때문입니다.
스레드에는 CPU에서 사용되는 하드웨어적 스레드가 있고, 프로그램에서 사용되는 소프트웨어적 스레드가 있습니다.
하드웨어적 스레드
스레드를 하드웨어적으로 정의하면 ‘하나의 코어가 동시에 처리하는 명령어 단위’를 의미합니다.
CPU에서 사용하는 스레드라는 용어는 보통 CPU 입장에서 정의된 하드웨어적 스레드를 의미합니다.
하나의 코어로 여러 명령어를 동시에 처리하는 CPU를 멀티스레드(multithread) 프로세서 또는 멀티스레드 CPU라고 합니다.
하이퍼스레딩(hyper-threading): 인텔의 멀티스레드 기술을 의미합니다.
인텔이 자신들의 멀티스레드 기술에 하이퍼스레딩이라는 명칭을 부여한 것입니다.
소프트웨어적 스레드
소프트웨어적으로 정의된 스레드는 ‘하나의 프로그램에서 독립적으로 실행되는 단위’를 의미합니다.
프로그래밍 언어나 운영체제를 학습할 때 접하는 스레드는 보통 이렇게 소프트웨어적으로 정의된 스레드를 의미합니다.
하나의 프로그램은 실행되는 과정에서 한 부분만 실행될 수도 있지만, 프로그램의 여러 부분이 동시에 실행될 수도 있습니다.
가령 워드 프로세서 프로그램을 개발한다고 가정해봅시다.
그리고 아래의 기능이 동시에 수행되길 원한다고 해 봅시다.
사용자로부터 입력받은 내용을 화면에 보여 주는 기능
사용자가 입력한 내용이 맞춤법에 맞는지 검사하는 기능
사용자가 입력한 내용을 수시로 저장하는 기능
이 기능들을 작동시키는 코드를 각각의 스레드로 만들면 동시에 실행할 수 있습니다.
정리하면, 스레드의 하드웨어적 정의는 ‘하나의 코어가 동시에 처리하는 명령어의 단위’를 의미하고, 소프트웨어적 정의는 ‘하나의 프로그램에서 독립적으로 실행되는 단위’를 의미합니다.
한 번에 하나씩 명령어를 처리하는 1코어 1스레드 CPU도 소프트웨어적 스레드를 수십 개 실행할 수 있습니다.
1 코어 1 스레드 CPU로도 프로그램의 여러 부분을 동시에 실행할 수 있습니다.
만약 스레드의 사전적 정의(실행 흐름의 단위)만을 암기한다면 ‘1코어 1스레드 CPU가 여러 스레드로 만들어진 프로그램을 실행할 수 있다’라는 말이 어려울 겁니다.
이런 이유로 하드웨어적 스레드와 소프트웨어적 스레드는 구분하여 기억하는 것이 좋습니다.
멀티스레드 프로세서
하나의 코어로 여러 명령어를 동시에 처리하는 기술인 하드웨어적 스레드를 “멀티스레드 프로세서” 라고 합니다.
멀티스레드 프로세서는 하나의 코어로 여러 명령어를 동시에 처리하는 CPU라고 했었습니다. 어떨게 이런 일이 가능할까요?
“멀티스레드 프로세서” 를 실제로 설계하는 일은 매우 복잡하지만, 가장 큰 핵심은 레지스터입니다.
하나의 코어로 여러 명령어를 동시에 처리하도록 만들려면 프로그램 카운터, 스택 포인터, 메모리 버퍼 레지스터, 메모리 주소 레지스터와 같이 하나의 명령어를 처리하기 위해 꼭 필요한 레지스터를 여러개 가지고 있으면 됩니다.
가열 프로그램 카운터가 두 개 있다면 ‘메모리에서 가져올 명령어 주소’를 두 개 지정할 수 있을 것이고, 스택 포인터가 두 개 있다면 두 개의 스택을 관리할 수 있을것 입니다.
아래의 그림을 봅시다.
하나의 명령어를 실행하기 위해 꼭 필요한 레지스터들을 편의상 ‘레지스터 세트’라고 표기했습니다.
레지스터 세트가 한 개인 CPU는 한 개의 명령어를 처리하기 위한 정보들을 기억할 뿐이지만, 레지스터 세트가 두 개인 CPU는 두 개의 명령어를 처리하기 위한 정보들을 기억할 수 있습니다.
여기서 ALU와 제어장치가 두 개의 레지스터 세트에 저장된 명령어를 해석하고 실행하면 하나의 코어에서 두 개의 명령어가 동시에 실행됩니다.
하드웨어 스레드를 이용해 하나의 코어로도 여러 명령어를 동시에 처리할 수 있습니다.
그러나 메모리 속 프로그램 입장에서 봤을 때 하드웨어 스레드는 마치 ‘한 번에 하나의 명령어를 처리하는 CPU’나 다름없습니다.
가령 2코어 4스레드 CPU는 한 번에 네 개의 명령어를 처리할 수 있는데, 프로그램 입장에서 봤을 땐 한 번에 하나의 명령어를 처리하는 CPU가 네 개 있는 것처럼 보입니다.
그래서 하드웨어 스레드를 논리 프로세서(logical processor) 라고 부르기도 합니다.
“코어” 는 명령어를 실행할 수 있는 ‘하드웨어 부품’이고, “스레드” 는 ‘명령어를 실행하는 단위’입니다.
“멀티코어 프로세서” 는 명령어를 실행할 수 있는 하드웨어 부품이 CPU 안에 두 개 이상 있는 CPU를 의미하고, “멀티스레드 프로세서” 는 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 의미합니다.
키워드로 정리하는 핵심 포인트
클럭 속도가 높은 CPU는 빠르게 작동합니다.
코어 란 CPU 내에서 명령어를 실행하는 부품입니다.
멀티코어 프로세서란 여러 개의 코어를 포함하는 CPU를 말합니다.
스레드에는 하드웨어적 스레드와 소프트웨어적 스레드가 있습니다.
멀티스레드 프로세서란 하나의 코어로 여러 개의 명령어를 동시에 실행할 수 있는 CPU를 말합니다.
-
-
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
래퍼 클래스 - 주요 메서드와 성능.
래퍼 클래스 - 주요 메서드.
래퍼 클래스가 제공하는 주요 메서드를 알아봅시다.
package lang.wrapper;
public class WrapperUtilsMain {
public static void main(String[] args) {
Integer i1 = Integer.valueOf(10); // 숫자, 래퍼 객체 변환.
Integer i2 = Integer.valueOf("10"); // 문자열, 래퍼 객체 변환.
int intValue = Integer.parseInt("10"); // 문자열 전용, 기본형 변환.
// 비교
int compareResult = i1.compareTo(20);
System.out.println("compareResult = " + compareResult);
// 산술 연산
System.out.println("sum: " + Integer.sum(10, 20));
System.out.println("min: " + Integer.min(10, 20));
System.out.println("max: " + Integer.max(10, 20));
}
}
실행 결과
compareResult = -1
sum: 30
min: 10
max: 20
valueOf(): 래퍼 타입을 반환합니다. 숫자, 문자열을 모두 지원합니다.
parseInt(): 문자열을 기본형으로 변환합니다.
compareTo(): 내 값과 인수로 넘어온 값을 비교합니다. 내 값이 크면 1, 같으면 0, 내 값이 작으면 -1을 반환합니다.
Integer.sum(), Integer.min(), Integer.max(): static 메서드 입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다.
pareInt() vs valueOf()
원하는 타입에 맞는 메서드를 사용하면 됩니다.
valueOf("10")는 래퍼 타입을 반환합니다.
parseInt("10")는 기본형을 반환합니다.
Long.parseLong() 처럼 각 타입에 parseXxx()가 존재합니다.
래퍼 클래스와 성능
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.
그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?
다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다.
package lang.wrapper;
public class WrapperVsPrimitive {
public static void main(String[] args) {
int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
long startTime, endTime;
// 기본형 long 사용
long sumPrimitive = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumPrimitive += i;
}
endTime = System.currentTimeMillis();
System.out.println("sumPrimitive = " + sumPrimitive);
System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
// 래퍼 클래스 Long 사용
Long sumWrapper = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumWrapper += i; // 오토 박싱 발생
}
endTime = System.currentTimeMillis();
System.out.println("sumWrapper = " + sumWrapper);
System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");
}
}
단순히 값을 반복해서 10억 번 더합니다.
기본형 long에 더하는 것과 래퍼 클래스 Long에 더하는 부분으로 나누어 테스트 합니다. 결과 값은 같습니다.
실행 결과 - M1 맥북 기준
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 381ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 1640ms
기본형 연산이 래퍼 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다. 참고로 계산 결과는 시스템 마다 다릅니다.
기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다. 예를 들어 int 는 보통 4바이트의 메모리를 사용합니다.
래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다. 자바 버전과 시스템마다 다르지만 대략 8-16바이트의 메모리를 추가로 사용합니다.
기본형, 래퍼 클래스 어떤 것을 사용?
이 연산은 10억 번의 연산을 수행했을 때 0.3초와, 1.5초의 차이입니다.
기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘다 매우 빠르게 연산이 수행됩니다.
0.3초 나누기 10억, 1.5초 나누기 10억이다.
일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다.
CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려합시다.
그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다.
유지보수 vs 최적화
유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다.
특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다.
코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다.
특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다.
권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
-
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 2
래퍼 클래스 - 기본형의 한계 2
기본형과 null
기본형은 항상 값을 가져야 합니다.
하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain0 {
public static void main(String[] args) {
int[] intArr = {-1, 0, 1, 2, 3};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // -1
}
private static int findValue(int[] intArr, int target) {
for (int value : intArr) {
if (value == target) {
return value;
}
}
return -1;
}
}
findValue()는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환합니다.
findValue()는 결과로 int를 반환합니다.
int와 같은 기본형은 항상 값이 있어야 합니다.
여기서도 값을 반환할 때 값을 찾지 못하면 숫자 중 하나를 반환해야 하는데 보통 -1 또는 0을 사용합니다.
실행 결과
-1
0
1
-1
실행 결과를 보면 입력값이 -1일 때 -1을 반환합니다.
그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환합니다.
입력값이 -1일 때를 생각해보면, 배열에 -1 값이 있어서 -1을 반환한 것인지, 아니면 -1 값이 없어서 -1을 반환한 것인지 명확하지 않습니다.
객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain1 {
public static void main(String[] args) {
MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // null
}
private static MyInteger findValue(MyInteger[] intArr, int target) {
for (MyInteger myInteger : intArr) {
if (myInteger.getValue() == target) {
return myInteger;
}
}
return null;
}
}
실행 결과
-1
0
1
null
앞서 만든 MyInteger 래퍼 클래서를 사용했습니다.
실행 결과를 보면 -1을 입력했을 때는 -1을 반환합니다.
100을 입력했을 때는 값이 없다는 null을 반환합니다.
기본형은 항상 값이 존재해야 합니다.
숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.
반면에 객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.
물론 null 값을 반환하는 경우 잘못하면 NullPointerException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
-
-
-
📝 [TIL] 240415 Today I Learned.
1. 리눅스 명령어
pwd(print working directory)
~ 은 Home이라는 경로
ls(list): 내 폴더 안에 있는 폴더 & 파일 내역을 보여줌
la -a(list all): 숨겨진 파일(보통 .으로 시작함)도 모두 볼 수 있음
cd 폴더명(change directory): 폴더 위치 이동
ls 명령어에서 확인된 폴더로 이동 가능
cd .. : 한 단계 위의 폴더라는 뜻
mkdir(make directory): 현재 경로에서 폴더를 생성
touch: 현재 경로에서 파일을 생성하는 명령어
정확히는 파일의 생성과 파일의 날짜, 시간을 변경하는 명령어
2.git
코드 변경점을 기록하는 것
버전 관리 도구(형상 관리 도구)
소프트웨어의 변경사항을 체계적으로 추적하고 통제하는 것
3. github
백업과 공유가 가능한 온라인 코드 저장소
협업이 가능한 온라인 코드 저장소
4. git 필수 명령어
코드 관리를 시작하는 명령어 - git init
초기화하다, 초기 세팅하다의 준말
프로젝트 시작 전 딱 한 번만 입력하면 됨
정확한 프로젝트 폴더(경로)에서 입력해야 함
코드를 저장하는 명령어 - git add & commit
git add 파일명: 저장하기 전 저장할 파일 “지정”
git commit -m “메세지 작성”: 실제로 파일을 “저장”
저장 여부 확인하는 명령어 - git status
내 프로젝트의 변경사항을 한 번에 지정하는 법 - git add .
working directory, staging area, repository에 대해서 알아봅시다.
저장 내역을 확인하는 명령어 - git log
커밋 메시지로 코드 변경점 추측 가능
git diff 코드 변경 확인
git reset 과거로 돌아가기 가능
변경 사항을 원격 저장소(예: github등)에 업로드 하는 명령어 - git push
git push <원격 저장소 이름> <브랜치 이름>
git push origin main
원격 저장소의 내용을 복사하여 새로운 로컬 저장소를 생성하는 데 사용하는 명령어 - git clone
새로운 프로젝트에 참여하거나 기존 프로젝트의 소스 코드를 로컬 컴퓨터로 가져오고 싶을 때 사용.
원격 저장소의 모든 파일, 디렉터리, 버전 기록을 포함합니다. 이를 통해 원격 저장소의 정확한 복사본을 로컬에 생성할 수 있습니다.
원격 저장소에 설정된 브랜치, 원격 추적 정보 등이 자동으로 설정됩니다. 이는 로털에서 작업을 시작하기 위해 필요한 초기 설정을 간소화합니다.
git clone <원격 저장소 URL>
특정 브랜치 클론: git clone -b <브랜치 이름> <원격 저장소 URL>
원격 저장소에서 최신 변경사항을 가져와서 현재 로컬 브랜치와 병합하는 데 사용하는 명령어 - git pull
이 명령은 git fetch와 git merge 두 단계의 작업을 한 번에 수행합니다.
이 명령어의 사용은 특히 팀 환경에서 다른 사람들의 작업을 지속적으로 로컬 환경에 통합할 필요가 있을 때 매우 유용합니다.
자세히 설명
‘git fetch’ 단계 : 이 단계에서는 원격 저장소의 최신 데이터를 로컬 저장소로 가져오지만, 현재 작업 중인 로컬 브랜치에는 자동으로 병합되지 않습니다. 원격 저장소의 변경사항은 로컬의 원격 추적 브랜치에 저장됩니다.
‘git merge’ 단계 : ‘git fetch’ 로 가져온 변경사항을 현재 작업 중인 브랜치와 병합합니다. 이 병합 과정을 통해 로컬 코드베이스에 원격 저장소의 최신 변경사항이 반영됩니다.
git pull <원격 저장소 이름> <브랜치 이름>
예를 들어, 원격 저장소 origin의 main 브랜치에서 최신 변경사항을 가져오고 싶다면 다음 명령어를 사용합니다.
git pull origin main
주의사항 및 활용 팁
자동 병합 충돌 : git pull 을 실행할 때 로컬에서 아직 커밋되지 않은 변경사항이 있다면, 원격의 변경사항과 충돌이 발생할 수 있습니다. 이 경우, Git은 사용자에게 충돌을 해결하고 커밋할 것을 요청합니다.
명시적인 병합 옵션 사용 : 병합 방식을 제어하고 싶을 때는 ’–rebase’ 옵션을 사용하여 기존 커밋 위에 원격 변경사항을 재배치할 수 있습니다. 이는 커밋 히스토리를 더 깔끔하게 유지하는 데 도움을 줍니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 1
래퍼 클래스 - 기본형의 한계 1
기본형의 한계
자바는 객체 지향 언어입니다.
그런데 자바 안에 객체가 아닌 것이 있습니다.
바로 int, double 같은 기본형(Primitive Type)입니다.
기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다.
객체가 아님: 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다. 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없습니다.
추가로 객체 참고가 필요한 컬렉션 프레임워크를 사용할 수 없습니다. 그리고 제네릭도 사용할 수 없습니다.
null 값을 가질 수 없음: 기본형 데이터 타입은 null 값을 가질 수 없습니다. 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다.
기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해봅시다.
왼쪽의 값이 더 작다 -1
두 값이 같다 0
왼쪽의 값이 더 크다 1
package lang.wrapper;
public class MyIntegerMethodMain0 {
public static void main(String[] args) {
int value = 10;
int i1 = compareTo(value, 5);
int i2 = compareTo(value, 10);
int i3 = compareTo(value, 20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
public static int compareTo(int value, int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
여기서는 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교합니다.
그런데 자기 자신인 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용됩니다.
이런 경우 만약 value가 객체라면 value 객체 스스로 가지 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다.
직접 만든 래퍼 클래스.
int를 클래스로 만들어 봅시다.
int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다.
다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다.
이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다.
package lang.wrapper;
public class MyInteger {
private final int value;
public MyInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public int compareTo(int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
@Override
public String toString() {
return String.valueOf(value);
}
}
MyInteger는 int value라는 단순한 기본형 변수를 하나 가지고 있습니다.
그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공합니다.
앞에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했습니다.
이 클래스는 불변으로 설계했습니다.
MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다.
덕분에 데이터 조각에 불과한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다.
package lang.wrapper;
public class MyIntegerMethodMain1 {
public static void main(String[] args) {
MyInteger myInteger = new MyInteger(10);
int i1 = myInteger.compareTo(5);
int i2 = myInteger.compareTo(10);
int i3 = myInteger.compareTo(20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
myInteger.compareTo()는 자기 자신의 값을 외부의 값과 비교합니다.
MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있습니다.
참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
-
💾 [CS] 명령어 사이클과 인터럽트
명령어 사이클과 인터럽트.
명령어 사이클 : CPU가 하나의 명령어를 처리하는 과정에는 어떤 정해진 흐름이 있고, CPU는 그 흐름을 반복하며 명령어를 처리해 나갑니다. 이렇게 하나의 명령어를 처리하는 정형화된 흐름을 “명령어 사이클” 이라고 합니다.
인터럽트 : CPU는 정해진 흐름에 따라 명령어를 처리해 나가지만, 이 흐름이 끊어지는 상황이 발생합니다. 이를 “인터럽트” 라고 합니다.
명령어 사이클
프로그램은 수많은 명령어로 이루어져있고, CPU는 이 명령어들을 하나씩 실행합니다.
이때 프로그램 속 각각의 명령어들은 일정한 주기가 반복되며 실행되는데, 이 주기를 명령어 사이클(instruction cycle) 이라고 합니다.
즉, 프로그램 속 각각의 명령어들은 명령어 사이클이 반복되며 실행됩니다.
메모리에 저장된 명령어 하나를 실행한다고 가정해 봅시다.
가장 먼저 해야할 것은 명령어를 메모리에서 CPU로 가져와야 합니다.
이게 명령어 사이클의 첫 번째 과정입니다.
인출 사이클(fetch cycle) : 메모리에 있는 명령어를 CPU로 가지고 오는 단계.
CPU로 명령어를 인출했다면 이제 명령어를 실행합니다.
이것이 명령어 사이클의 두 번째 과정입니다.
실행 사이클(execution cycle) : CPU로 가져온 명령어를 실행하는 단계, 제어장치가 명령어 레지스터에 담긴 값을 해석하고, 제어 신호를 발생시키는 단계.
프로그램을 이루는 수많은 명령어는 일반적으로 인출과 실행 사이클을 반복하며 실행됩니다.
즉, CPU는 프로그램 속 명령어를 가져오고 실행하고, 또 가져오고 실행하고를 반복하는 것입니다.
하지만 모든 명령어가 이렇게 간단히 실행되는 건 아닙니다.
명령어를 인출하여 CPU로 가져왔다하더라도 곧바로 실행할 수 없는 경우도 있기 때문입니다.
예를 들어 간접 주소 지정 방식을 생각해 봅시다.
간접 주소 지정 방식은 오퍼랜드 필드에 유효 주소의 주소를 명시한다고 했습니다.
이 경우 명령어를 인출하여 CPU로 가져왔다 하더라도 바로 실행 사이클에 돌입할 수 없습니다.
명령어를 실행하기 위해서는 메모리 접근을 한 번 더 해야 하기 때문입니다.
이 단계를 간접 사이클(indirect cycle) 이라고 합니다.
인터럽트.
프로그램을 개발하다 보면 아래 인터럽트라는 단어를 쉽게 접할 수 있습니다.
인터럽트는 영어로 interrupt이며, ‘방해하다, 중단시키다’를 의미합니다.
즉, CPU가 수행 중인 작업은 방해를 받아 잠시 중단될 수 있는데, 이렇게 CPU의 작업을 방해하는 신호를 인터럽트(interrupt) 라고 합니다.
CPU가 작업을 잠시 중단해야 할 정도라면 인터럽트는 ‘CPU가 꼭 주목해야 할 때’ 혹은 ‘CPU가 얼른 처리해야 할 다른 작업이 생겼을 때’ 발생합니다.
인터럽트의 종류에는 크게 동기 인터럽트와 비동기 인터럽트가 있습니다.
동기 인터럽트(synchronous interrupt) : CPU에 의해 발생하는 인터럽트입니다.
CPU가 명령어들을 수행하다가 예상치 못한 상황에 마주쳤을 때, 가령 CPU가 실행하는 프로그래밍상의 오류와 같은 예외적인 상황에 마추쳤을 때 발생하는 인터럽트입니다.
이런 점에서 동기 인터럽트는 예외(execption) 라고 부릅니다.
비동기 인터럽트(asynchronous interrupt) : 주로 입출력장치에 의해 발생하는 인터럽트입니다.
입출력장치에 의한 비동기 인터럽트는 세탁기 완료 알리므 전자레인지 조리 완료 알림과 같은 알림 역할을 합니다.
구체적으로 다음과 같이 사용됩니다.
CPU가 프린터와 같은 입출력장치에 입출력 작업을 부탁하면 작업을 끝낸 입출력장치가 CPU에 완료 알림(인터럽트)을 보냅니다.
키보드, 마우스와 같은 입출력 장치가 어떠한 입력을 받아들였을 때 이를 처리하기 위해 CPU에 입력 알림(인터럽트)을 보냅니다.
하드웨어 인터럽트
하드웨어 인터럽트는 알림과 같은 인터럽트 입니다.
CPU는 입출력 작업 도중에도 효율적으로 명령어를 처리하기 위해 이런 알림과 같은 하드웨어 인터럽트를 사용합니다.
하드웨어 인터럽트를 이용하면 CPU는 주기적으로 하드웨어 완료 여부를 확인할 필요가 없습니다.
CPU는 하드웨어로부터 하드웨어 완료 인터럽트를 받을 때까지 다른 작업을 처리할 수 있습니다.
이렇듯 하드웨어 인터럽트는 입출력 작업 중에도 CPU로 하여금 효율적으로 명령어를 처리할 수 있게 합니다.
하드웨어 인터럽트 처리 순서
입출력장치는 CPU에 인터럽트 요청 신호를 보냅니다.
CPU는 실행 사이클이 끝나고 명령어를 인출하기 전 항상 인터럽트 여부를 확인합니다.
CPU는 인터럽트 요청을 확인하고 인터럽트 플래그를 통해 현재 인터럽트를 받아들일 수 있는지 여부를 확인합니다.
인터럽트를 받아들일 수 있다면 CPU는 지금까지의 작업을 백업합니다.
CPU는 인터럽트 백터를 참조하여 인터럽트 서비스 루틴을 실행합니다.
인터럽트 서비스 루틴이 끝나면 4에서 백업해 둔 작업을 복구하여 실행을 재개합니다.
인터럽트 요청 신호 : 인터럽트는 CPU의 정상적인 실행 흐름을 끊는 것이기에 다른 누군가가 인터럽트하기 전에 “지금끼어들어도 되나요?” 하고 CPU에 물어봐야 합니다. 이를 인터럽트 요청 신호라고 합니다.
이때, CPU가 인터럽트 요청을 수용하기 위해서는 플래그 레지스터의 인터럽트 플래그(interrupt flag) 가 활성화되어 있어야 합니다.
인터럽트 플래그는 말 그래도 하드웨어 인터럽트를 받아들일지, 무시할지를 결정하는 플래그입니다.
CPU가 중요한 작업을 처리해야 하거나 어떤 방해도 받지 않아야 할 때 인터럽트 플래그는 불가능으로 설정됩니다.
만약 인터럽트 플래그가 ‘불가능’으로 설정되어 있다면 CPU는 인터럽트 요청이 오더라도 해당 요청을 무시합니다.
반대로 인터럽트 플래그가 ‘가능’으로 설정되어 있다면 CPU는 인터럽트 요청 신호를 받아들이고 인터럽트를 처리합니다.
다만, 모든 하드웨어 인터럽트를 인터럽트 플래그로 막을 수 있는 것은 아닙니다.
인터럽트 플래그가 불가능으로 설정되어 있을지라도 무시할 수 없는 인터럽트 요청도 있습니다.
무시할 수 없는 하드웨어 인터럽트 가장 우선순위가 높은, 다시 말해 반드시 가장 먼저 처리해야 하는 인터럽트입니다.
정전이나 하드웨어 고장으로 인한 인터럽트가 이에 해당합니다.
CPU가 인터럽트 요청을 받아들이기로 했다면 CPU는 서비스 루틴이라는 프로그램을 실행합니다.
인터럽트 서비스 루틴(ISB: Interrupt Service Routine): 인터럽트를 처리하기 위한 프로그램. 인터럽트 핸들러(Interrupt handler) 라고도 불립니다.
어떤 인터럽트가 발생했을 때 해당 인터럽트를 어떻게 처리하고 작동해야 할지에 대한 정보로 이루어진 프로그램입니다.
요컨태 ‘CPU가 인터럽트를 처리한다’는 말은 ‘인터럽트 서비스 루틴을 실행하고, 본래 수행하던 작업으로 다시 되돌아온다’ 라는 말과 같습니다.
인터럽트를 처리하는 방법은 입출력장치마다 다르므로 각기 다른 인터럽트 서비스 루틴을 가지고 있습니다.
즉, 메모리에는 위 그림처럼 여러 개의 인터럽트 서비스 루틴이 저장되어 있습니다.
이들 하나하나가 ‘인터럽트가 발생하면 어떻게 행동해야 할지를 알려주는 프로그램’이라고 보면 됩니다.
인터럽트 벡터(Interrupt vector) : CPU는 수많은 인터럽트 서비스 루틴을 구분하기 위해 인터럽트 벡터를 이용합니다. 인터럽트 서비스 루틴을 식별하기 위한 정보입니다.
인터럽트 벡터를 알면 인터럽트 서비스 루틴의 시작 주소를 알 수 있기 때문에 CPU는 인터럽트 벡터를 통해 특정 인터럽트 서비스 루틴을 처음부터 실행할 수 있습니다.
CPU는 하드웨어 인터럽트 요청을 보낸 대상으로부터 데이터 버스를 통해 인터럽트 벡터를 전달받습니다.
가령, CPU가 작업을 수행하는 도중 키보드 인터럽트가 발생한 경우라면 CPU는 인터럽트 벡터를 참조하여 키보드 인터럽트 서비스 루틴의 시작 주소를 알아내고, 이 시작 주소부터 실행해 나가며 키보드 인터럽트 서비스 루틴을 실행합니다.
정리하면 ‘CPU가 인터럽트를 처리한다’는 말은 ‘인터럽트 서비스 루틴을 실행하고, 본래 수행하던 작업으로 다시 되돌아온다’는 말과 같습니다.
그리고 CPU가 인터럽트 서비스 루틴을 실행하려면 인터럽트 서비스 루틴의 시작 주소를 알아야 하는데, 이는 인터럽트 벡터를 통해 알 수 있습니다.
인터럽트 서비스 루틴은 여느 프로그램과 마찬가지로 명령어와 데이터로 이루어져 있습니다.
그렇기에 인터럽트 서비스 루틴도 프로그램 카운터를 비롯한 레지스터들을 사용하며 실행됩니다.
그럼, 인터럽트가 발생하기 전까지 레지스터에 저장되어 있던 값들은 어떻게 할까요?
인터럽트 요청을 받기 전까지 CPU가 수행하고 있었던 일은 인터럽트 서비스 루틴이 끝나면 되돌아와서 마저 수행을 해야 하기 때문에 지금까지의 작업 내역들은 어딘가에 백업을 해둬야 합니다.
그렇기에 CPU는 인터럽트 서비스 루틴을 실행하기 전에 프로그램 카운터 값 등 현재 프로그램을 재개하기 위해 필요한 모든 내용을 스택에 백업합니다.
그러고 나서 인터럽트 서비스 루틴의 시작 주소가 위치한 곳으로 프로그램 카운터 값을 갱신하고 인터럽트 서비스 루틴을 실행합니다.
인터럽트 서비스 루틴을 모두 실행하면, 다시 말해 인터럽트를 처리하고 나면 스택에 저장해 둔 값을 다시 불러온 뒤 이전까지 수행하던 작업을 재개합니다.
키워드 정리
인터럽트 요청 신호 : CPU의 작업을 방해하는 인터럽트에 대한 요청
인터럽트 플래그 : 인터럽트 요청 신호를 받아들일지 무시할지를 결정하는 비트
인터럽트 벡터 : 인터럽트 서비스 루틴의 시작 주소를 포함하는 인터럽트 서비스 루틴의 식별 정보
인터럽트 서비스 루틴 : 인터럽트를 처리하는 프로그램
CPU는 이와 같은 과정을 반복해 나가며 프로그램을 실행한다고 볼 수 있습니다.
키워드로 정리하는 핵심 포인트
명령어 사이클은 하나의 명령어가 처리되는 주기로, 인출, 실행, 간접, 인터럽트 사이클로 구성되어 있습니다.
인터럽트 는 CPU의 정상적인 작업을 방해하는 신호입니다.
인터럽트의 종류에는 예외와 하드웨어 인터럽트가 있습니다.
인터럽트 서비스 루틴은 인터럽트를 처리하기 위한 동작들로 이루어진 프로그램입니다.
-
☕️[Java] String 클래스 - 정리
String 클래스 - 정리
1. 자바에서 문자를 다루는 대표적인 타입
char
String
char 배열을 사용하면 문자열을 다룰 수 있으나 불편합니다.
그래서 String이라는 것을 Java에서 제공해줍니다.
2. String 클래스를 통해 문자열을 생성하는 2가지 방법.
String str1 = "hello"; // 방법 1 -> 문자열 리터럴
String str2 = new String("hello"); // 방법 2
쌍따옴표 사용: 방법 1
객체 생성 : 방법 2
문자열 리터럴을 사용하더라도 결국 new String("hello"); 가 되는 것 입니다.
편의상 쌍따옴표로 문자열을 감싸면(문자열 리터럴) 자바 언어에서 new String("hello"); 와 같이 변경해 줍니다.
이 경우 실제로는 성정 최적화를 위해 문자열 풀을 사용합니다.
문자열은 참조형입니다.
3. String 클래스 내부
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
3. String 클래스 비교.
String 클래스를 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야합니다.
동일성(Identity) : == 연산자를 사용해서 두 객체의 참조(Reference) 가 동일한 객체를 가리키고 있는지 확인합니다.
동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인합니다.
4. 문자열 리터럴, 문자열 풀.
String str3 = "hello" 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용합니다.
자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어 둡니다.
이때 같은 문자열이 있으면 만들지 않습니다.
String str3 - "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾습니다.
그리고 찾은 인스턴스의 참조(x003)를 반환합니다.
String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용합니다.
문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있습니다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공합니다.
참고
풀(Pool)은 자원이 모여있는 곳을 의미합니다.
프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻합니다.
여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적입니다.
대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화할 수 있습니다.
참고로 문자열 풀은 힙 영역을 사용합니다.
그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있습니다.
5. String 클래스는 불변객체.
String은 불변 객체입니다.
따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없습니다.
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1.concat(" java");
System.out.println("str1 = " + str1);
System.out.println("str1 = " + str2);
}
String은 불변 객체입니다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환합니다.
실행 결과
str1 = hello
str2 = hello java
String.concat()은 내부에서 새로운 String 객체를 만들어서 반환합니다.
따라서 불변과 기존 객체의 값을 유지합니다.
문자열에서 무언가 변경하는 것이 있으면 그 내부에서 인스턴스를 생성하여 반환합니다.
때문에 반환값을 받아서 사용해야 합니다.
6. String 클래스가 불변으로 설계된 이유.
사이드 이팩트 문제.
문자열 풀을 사용시에 불변으로 설계되어 있어야 안전하게 사용할 수 있기 때문입니다.
7. String 클래스 주요 메서드.
주요 메서드 블로그 글 1
주요 메서드 블로그 글 2
참고. CharSequence
CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
8. StringBuilder - 가변 String
불변인 String 클래스의 단점: 뭔가 값을 더하거나 변경하거나 할 때마다 계속 새로운 객체를 만들어내야 한다는 점 입니다. 때문에 성능도 느려질 수 있습니다. (물론, 불변이기 때문에 안전하기는 합니다.)
이러한 단점들 때문에 StringBuilder 가 있습니다.
StringBuilder: 가변 String 입니다.
값을 쭉 바꾸고 쓰면 되는데, 마지막에는 다시 String으로 바꾸는 toString()을 사용합니다.
즉, 다시 안전한 불변으로 바꾸는 작업입니다.
변경이 필요할 때 가변으로, 마지막에는 불변으로 바꿔서 쓰는것을 권장합니다.
“뭔가 문자열을 변경할 일이 많다” -> StringBuilder 를 사용하면 됩니다.
9. String 최적화.
그러나 생각보다 StringBuilder를 사용할 때가 많이 없습니다.
그 이유는 자바가 String을 최적화하기 때문입니다.
컴파일러에서도 최적화를 하고, 변수로 되어있어도 컴파일러가 최적화를 수행합니다.
예를 들어 Java가 직접 StringBuilder를 사용합니다.
String result = new StringBuilder().append(str1).append(str2).toString();
10. String 최적화가 어려운 경우.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 1000000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
이럴 때는 직접 StringBuilder 를 사용하면 됩니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
String result = sb.toString();
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
11. 메서드 체이닝.
자기 자신의 값을 반환해서 메서드를 쭉 연결해서 사용할 수 있습니다.
자바의 많은 라이브러리들이 메서드 체이닝 기법을 사용하고 있습니다.
메서드 체이닝 블로그 글
-
☕️[Java] String 최적화
String 최적화.
자바의 String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐줍니다.
문자열 리터럴 최적화
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행합니다.(최적화 방식은 자바 버전에 따라 달라집니다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다.
대신에 문자열을 더하기 연산(+)을 사용하면 충분합니다.
String 최적화가 어려운 경우
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
package lang.string.builder;
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대락 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java ...
time = 2528ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.5초가 걸렸습니다.
이럴 때는 직접 StringBuilder를 사용하면 됩니다.
package lang.string.builder;
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java ...
time = 4ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.004초가 걸렸습니다.
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
☕️[Java] 메서드 체이닝 - Method Chaining
메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해서 알아봅시다.
package lang.string.chaining;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다. 이 부분을 유의해서 봅시다.
package lang.string.chaining;
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력합니다.
여기서는 add() 메서드의 반환값은 사용하지 않았습니다.
이번에는 add() 메서드의 반환값을 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같습니다.
adder.add(1)을 호출합니다.
add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다.
adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다.
add()메서드는 자기 자신(this) 메서드의 참조값을 반환합니다.
이 반환값을 adder1, adder2, adder3에 보관했습니다.
따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다.
왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 더 잘 읽히지 않습니다.
이런 방식을 왜 사용하는 것 일까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다.
이 반환된 참조값을 변수에 담아두지 않아도 됩니다.
대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다.
다음과 같은 순서로 실행됩니다.
adder.add(1).add(2).add(3).getValue(); // value = 0
x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다.
x001.getValue(); // value = 6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다.
코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다.
마치 메서드가 체인으로 연결된 것 처럼 보입니다.
이러한 기법을 메서드 체이닝이라고 합니다.
물론 실행 결과도 기존과 동일합니다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다. 예) adder.add(1), adder.add(2)
매서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다. 예) insert(), delete(), reverse()
앞서 StringBuilder를 사용한 코드는 다음과 같이 개선할 수 있습니다.
package lang.string.builder;
public class StringBuilderMain1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4,8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
정리
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리합니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
💾 [CS] 레지스터
레지스터.
프로그램 속 명령어와 데이터는 실행 전후로 반드시 레지스터에 저장됩니다.
따라서 레지스터에 저장된 값만 잘 관찰해도 프로그램의 실행 흐름을 파악할 수 있습니다
다시 말해 레지스터 속 값을 유심히 관찰하면 프로그램을 실행할 때 CPU 내에서 무슨 일이 벌어지고 있는지, 어떤 명령어가 어떻게 수행되는지 알 수 있습니다.
반드시 알아야 할 레지스터.
프로그램 카운터
명령어 레지스터
메모리 주소 레지스터
메모리 버퍼 레지스터
플래스 레지스터
범용 레지스터
스택 포인터
베이스 레지스터
프로그램 카운터.
프로그램 카운터(PC: Program Counter) : 메모리에서 가져올 명령어의 주소, 즉 메모리에서 읽어 들일 명령어의 주소를 저장합니다.
프로그램 카운터를 명령어 포인터(IP: Instruction Pointer) 라고 부르는 CPU도 있습니다.
명령어 레지스터.
명령어 레지스터(IR: Instruction Register) : 해석할 명령어, 즉 방금 메로미에서 읽어 들인 명령어를 저장하는 레지스터입니다.
제어장치는 명령어를 레지스터 속 명령어를 받아들이고 이를 해석한 뒤 제어 신호를 내보냅니다.
메모리 주소 레지스터.
메모리 주소 레지스터(MAR: Memory Address Register) : 메모리 주소를 저장하는 레지스터입니다. CPU가 읽어 들이고자 하는 주소 값을 주소 버스로 보낼 때 메모리 주소 레지스터를 거치게 됩니다.
메모리 버퍼 레지스터.
메모리 버퍼 레지스터(MBR: Memory buffer register) : 메모리와 주고받을 값(데이터와 명령어)을 저장하는 레지스터입니다.
즉, 메모리에 쓰고 싶은 값이나 메모리로부터 전달받은 값은 메모리 버퍼 레지스터를 거칩니다.
CPU가 주소 버스로 내보낼 값이 메모리 주소 레지스터를 거친다면, 데이터 버스로 주고 받을 값은 메모리 버퍼 레지스터를 거칩니다.
메모리 버퍼 레지스터는 메모리 데이터 레지스터(MDR: Memory Data Register)라고도 불립니다.
메모리에 저장된 프로그램을 실행하는 과정에서 프로그램 카운터, 명령어 레지스터, 메모리 주소 레지스터, 메모리 버퍼 레지스터에 어떤 값들이 담기는지 알아봅시다.
1.
CPU로 실행할 프로그램이 1000번지부터 1500번지까지 저장되어 있다고 가정하겠습니다,
그리고 1000번지에는 1101₍₂₎이 저장되어 있다고 가정하겠습니다.
2.
프로그램을 처음부터 실행하기 위해 프로그램 카운터에는 1000이 저장됩니다.
이는 메모리에서 가져올 명령어가 1000번지에 있다는 걸 의미합니다.
3.
1000번지를 읽어 들이기 위해서는 주소 버스로 100번지를 내보내야 합니다.
이를 위해 메모리 주소 레지스터에는 1000이 저장됩니다.
4.
‘메모리 읽기’ 제어 신호와 메모리 주소 레지스터 값이 각각 제어 버스와 주소 버스를 통해 메모리로 보내집니다.
5.
메모리 1000번지에 저장된 값은 데이터 버스를 통해 메모리 버퍼 레지스터로 전달되고, 프로그램 카운터는 증가되어 다음 명령어를 읽어 들일 준비를 합니다.
6.
메모리 버퍼 레지스터에 저장된 값은 명령어 레지스터로 이동합니다.
7.
제어장치는 명령어 레지스터의 명령어를 해석하고 제어 신호를 발생시킵니다.
5단계에서 프로그램 카운터 값이 증가한 것을 확인했습니다.
프로그램 카운터 값이 증가했으니 1000번지 명령어 처리가 끝나면 CPU는 다음 명령어(1001번지)를 읽어 들입니다.
이처럼 프로그램 카운터는 지속적으로 증가하며 계속해서 다음 명령어를 읽어 들일 준비를 합니다.
이 과정이 반복되면서 CPU는 프로그램을 차례대로 실행해 나갑니다.
결국 CPU가 메모리 속 프로그램을 순차적으로 읽어 들이고 실행해 나갈 수 있는 이유는 CPU 속 프로그램 카운터가 꾸준히 증가하기 때문입니다.
범용 레지스터
범용 레지스터(general purpose register) : 다양하고 일반적인 상황에서 자유롭게 사용할 수 있는 레지스터입니다.
메모리 버퍼 레지스터는 테이터 버스로 주고받을 값만 저장하고, 메모리 주소 레지스터는 주소 버스로 내보낼 주소값만 저장하지만, 범용 레지스터는 데이터와 주소를 모두 저장할 수 있습니다.
일반적으로 CPU 안에는 여러 개의 범용 레지스터들이 있고, 현대 대다수 CPU는 모두 범용 레지스터를 가지고 있습니다.
플레그 레지스터
플래그 레지스터(Flag register) : 연산 결과 또는 CPU 상태에 대한 부가적인 정보를 저장하는 레지스터입니다.
특정 레지스터를 이용한 주소 지정 방식(1): 스택 주소 지정 방식.
스택 주소 지정 방식 : 스택과 스택 포인터를 이용한 주소 지정 방식
스택은 한쪽 끝이 막혀 있는 통과 같은 저장 공간입니다.
그래서 스택은 가장 최근에 저장하는 값부터 꺼낼 수 있습니다.
여기서 스택 포인터란 스택의 꼭대기를 가리키는 레지스터입니다.
즉, 스택 포인터는 스택에 마지막으로 저장한 값의 위치를 저장하는 레지스터입니다.
예를 들어 봅시다.
가령 다음과 같이 위에서부터 주소가 매겨져 있고 아래부터 차곡차곡 데이터가 저장되어 있는 스택이 있다고 가정해봅시다.
이때 스택 포인터는 스택의 제일 꼭대기 주소, 즉 4번지를 저장하고 있습니다.
이는 ‘스택 포인터가 스택의 꼭대기를 가리키고 있다’고 볼 수 있겠죠.
쉽게 말해, 스택 포인터는 스택의 어디까지 데이터가 캐워져 있는지에 대한 표시라고 보면 됩니다.
그럼 이 스택에서 데이터를 꺼낼 때는 어떤 데이터부터 꺼내게 될까요?
1 -> 2 -> 3 순서대로 꺼낼 수 있습니다.
여기서 하나의 데이터를 꺼내면 스택에는 2와 3이 남고, 스택의 꼭대기 주소가 달라졌기 때문에 스택 포인터는 5번지를 가리킵니다.
반대로 스택에 데이터를 추가한다면 어떻게 될까요?
현재 스텍세 4라는 데이터를 저장하면 스택의 꼭대기에 4가 저장됩니다.
이때 스택의 꼭대기 주소가 달라졌기 때문에 스택 포인터는 4번지를 가리킵니다.
그런데 스택이라는 것은 도대체 어디에 있는 걸까요?
스택은 메모리 안에 있습니다.
정확히는 메모리 안에 스택처럼 사용할 영역이 정해져 있습니다.
이를 스택 영역이라고 합니다.
이 영역은 다른 주소 공간과는 다르게 스택처럼 사용하기 암묵적으로 약속된 영역입니다.
특정 레지스터를 이용한 주소 지정 방식(2): 변위 주소 지정 방식
변위 주소 지정 방식(displacement addressing mode) : 오퍼랜드 필드의 값(변위)과 특정 레지스터의 값을 더하여 유효 주소를 얻어내는 주소 지정 방식입니다.
그래서 변위 주소 지정방식을 사용하는 명령어는 다음 그림과 같이 연산 코드 필드, 어떤 레지스터의 값과 더할지를 나타내는 레지스터 필드, 그리고 주소를 담고있는 오퍼랜드 필드가 있습니다.
이때, 변위 주소 지정 방식은 오퍼랜드 필드의 주소와 어떤 레지스터를 더하는지에 따라 상대 주소 지정 방식, 베이스 레지스터 주소 지정 방식 등으로 나뉩니다.
상대 주소 지정 방식
상대 주소 지정 방식(relative addressing mode) : 오퍼랜드와 프로그램 카운터와 값을 더하여 유효 주소를 얻는 방식입니다.
프로그램 카운터에는 읽어 들일 명령어의 주소가 저장되어 있습니다.
만약 오퍼랜드가 음수, 가령 -3이였다면 CPU는 읽어 들이기로 한 명령어로부터 ‘세 번째 이전’ 번지로 접근합니다.
한마디로 실행하려는 명령어의 세 칸 이전 번지 명령어를 실행하는 것이지요
반면, 오퍼랜드가 양수, 가열 3이었다면 CPU는 읽어 들이기로 했던 명령어의 ‘세 번째 이후’ 번지로 접근합니다.
즉, 실행하려는 명령어에서 세 칸 건너뛴 번지를 실행하는 겁니다.
상대 주소 지정 방식은 프로그래밍 언어의 if문과 유사하게 모든 코드를 실행하는 것이 아닌, 분기하여 특정 주소의 코드를 실행할 때 사용됩니다.
베이스 레지스터 주소 지정 방식
베이스 레지스터 주소 지정 방식(base-register addressing mode) : 오퍼랜드와 베이스 레지스터의 값을 더하여 유효 주소를 얻는 방식입니다.
여기서 베이스 레지스터는 ‘기준 주소’, 오퍼랜드는 ‘기준 주소로부터 떨어진 거리’로서의 역할을 합니다.
즉, 베이스 레지스터 주소 지정 방식은 베이스 레지스터 속 기준 주소로부터 얼마나 떨어져 있는 주소에 접근할 것인지를 연산하여 유효 주소를 얻어내는 방식입니다.
가령 베이스 레지스터에 200이라는 값이 있고 오퍼랜드가 40이라면 이는 “기준 주소 200번지로부터 40만큼 떨어진 240번지로 접근하라”를 의미합니다.
또 베이스 레지스터에 550이라는 값이 담겨 있고 오퍼랜드가 50이라면 이는 “기준 주소 550번지로부터 50만큼 떨어진 600번지로 접근하라”를 의미하는 명령어 입니다.
키워드로 정리하는 핵심 포인트
프로그램 카운터 는 메모리에서 가져올 명령어의 주소, 명령어 레지스터는 해석할 명령어를 저장합니다.
메모리 주소 레지스터는 메모리의 주소, 메모리 버퍼 레지스터는 메모리와 주고받을 데이터를 저장합니다.
범용 레지스터는 데이터와 주소를 모두 저장하고, 플래그 레지스터는 연산 결과 혹은 CPU 상태에 대한 부가 정보를 저장합니다.
스택 포인터는 스택 최상단의 위치를 저장합니다.
베이스 레지스터에 저장된 주소는 기준 주소로서의 역할을 합니다.
더 알아보기
Jump
Jump 명령어는 프로그램의 실행 흐름을 끊고 지정된 주소로 점프합니다.
Conditional Jump
Conditional Jump 명령어는 특정 조건이 충족될 때에만 주어진 주소로 점프합니다.
Call
Call 명령어는 현재 위치를 저장하고 지정된 주소로 이동합니다.
현재 위치를 저장하기 위해 스택을 사용합니다.
주로 서브루틴(하위 루틴 또는 함수)을 호출할 때 사용됩니다.
Return
Return 명령어는 서브루틴에서 호출자로 복귀합니다.
호출된 서브루틴이 실행을 마치고 호출자로 돌아갈 때 사용됩니다.
-
☕️[Java] String 클래스 - 주요 메서드 2
String 클래스 - 주요 메서드 2
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
package lang.string.method;
public class StringChangeMain2 {
public static void main(String[] args) {
String strWithSpaces = " Java Programming ";
System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase());
System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase());
System.out.println("공백 제거(trim): '" + strWithSpaces.trim() + "'");
System.out.println("공백 제거(strip): '" + strWithSpaces.strip() + "'");
System.out.println("앞 공백 제거(stripLeading): '" + strWithSpaces.stripLeading() + "'");
System.out.println("뒤 공백 제거(stripTrailing): '" + strWithSpaces.stripTrailing() + "'");
}
}
실행 결과
소문자로 변환: java programming
대문자로 변환: JAVA PROGRAMMING
공백 제거(trim): 'Java Programming'
공백 제거(strip): 'Java Programming'
앞 공백 제거(stripLeading): 'Java Programming '
뒤 공백 제거(stripTrailing): ' Java Programming'
문자열 분할 및 조합
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
package lang.string.method;
public class StringSplitJoinMain {
public static void main(String[] args) {
String str = "Apple,Banana,Orange";
// split()
String[] splitStr = str.split(",");
for (String s : splitStr) {
System.out.println(s);
}
String joinStr = "";
for (int i = 0; i < splitStr.length; i++) {
String string = splitStr[i];
joinStr += string;
if (i != splitStr.length-1) {
joinStr += "-";
}
}
System.out.println("joinStr = " + joinStr);
// join()
String joinedStr = String.join("-", "A", "B", "C");
System.out.println("연결된 문자열 = " + joinedStr);
// 문자열 배열 연결
String result = String.join("-", splitStr);
System.out.println("result = " + result);
}
}
실행 결과
Apple
Banana
Orange
joinStr = Apple-Banana-Orange
연결된 문자열 = A-B-C
result = Apple-Banana-Orange
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
package lang.string.method;
public class StringUtilsMain1 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
Object obj = new Object();
String str = "Hello, Java!";
// valueOf 메서드
String numString = String.valueOf(num);
System.out.println("숫자의 문자열 값: " + numString);
String boolString = String.valueOf(bool);
System.out.println("불리언의 문자열 값: " + boolString);
String objString = String.valueOf(obj);
System.out.println("객체의 문자열 값: " + objString);
// 문자 + x -> 문자
String numString2 = "" + num;
System.out.println("빈 문자열 + num: " + numString2);
// toCharArray 메서드
char[] strCharArray = str.toCharArray();
System.out.println("문자열을 문자 배열로 변환 : " + strCharArray);
for (char c : strCharArray) {
System.out.print(c);
}
System.out.println();
}
}
실행 결과
숫자의 문자열 값: 100
불리언의 문자열 값: true
객체의 문자열 값: java.lang.Object@a09ee92
빈 문자열 + num: 100
문자열을 문자 배열로 변환 : [C@30f39991
Hello, Java!
package lang.string.method;
public class StringUtilsMain2 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
String str = "Hello, Java!";
// format 메서드
String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
System.out.println(format1);
String format2 = String.format("숫자: %.2f ", 10.1234);
System.out.println(format2);
//printf
System.out.printf("숫자: %.2f\n", 10.1234);
// matches 메서드
String regex = "Hello, (Java!|World)";
System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
}
}
실행 결과
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true
format 메서드에서 %d는 숫자 %b는 boolean, %s는 문자열을 뜻합니다.
-
☕️[Java] StringBuilder - 가변 String
StringBuilder - 가변 String.
불변인 String 클래스의 단점.
불변인 String 클래스에도 단점이 있습니다.
다음 예를 봅시다.
참고로 실제로 작동하는 코드는 아닙니다.
두 문자를 더하는 경우 다음과 같이 작동합니다.
"A" + "B";
String("A") + String("B"); // 문자는 String 타입.
String("A").concat(String("B")); // 문자의 더하기는 concat을 사용.
new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다.
불변인 String의 내부 값은 변경할 수 없습니다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다.
더 많은 문자를 더하는 경우를 살펴봅시다.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
이 경우 총 3개의 String 클래스가 추가로 생성됩니다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC")는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD")만 사용됩니다.
결과적으로 중간에 만들어진 new String("AB)", new String("ABC")는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점입니다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다.
결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고 : 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
char[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
StringBuilder 사용 예
실제 StringBuilder를 어떻게 사용하는지 확인해 봅시다.
package lang.string.builder;
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4,8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuilder(가변) -> String(불변)으로 바꿀 수 있음.
String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성합니다.
append() 메서드를 사용해 여러 문자열을 추가합니다.
insert() 메서드로 특정 위치에 문자열을 삽입합니다.
delete() 메서드로 특정 범위의 문자열을 삭제합니다.
reverse() 메서드로 문자열을 뒤집습니다.
마지막으로 toString() 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다.
실행 결과
sb = ABCD
sb = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
가변(Mutable) vs 불변(Immutable)
String은 불변합니다. 즉, 한 번 생성되면 그 내용을 변경할 수 없습니다.
따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다.
이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
반면에, StringBuilder는 가변적입니다.
하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않습니다.
이로 인해 메모리 사용을 줄이고 성늘을 향상시킬 수 있습니다. 단 사이드 이펙트를 주의해야 합니다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
-
☕️[Java] String 클래스 - 주요 메서드 1
String 클래스 - 주요 메서드 1
주요 메서드 목록
String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공합니다.
여기서는 자주 사용하는 기능 위주로 나열했습니다.
참고로 기능이 너무 많기 때문에 메서드를 외우기 보다는 주로 사용하는 메서드가 이런 것이 있구나 대략 알아두고, 필요할 때 검색하거나 API 문서를 통해서 원하는 기능을 찾는 것이 좋습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
문자열 분할 및 조합.
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
이제 본격적으로 하나씩 알아봅시다.
참고: CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
package lang.string.method;
public class StringInfoMain {
public static void main(String[] args) {
String str = "Hello, Java!";
System.out.println("문자열의 길이: " + str.length());
System.out.println("문자열이 비어 있는지: " + str.isEmpty());
System.out.println("문자열이 비어 있거나 공백인지 1: " + str.isBlank());
System.out.println("문자열이 비어 있거나 공백인지 2: " + " ".isBlank());
char c = str.charAt(7);
System.out.println("7번째 인덱스의 문자 = " + c);
}
}
실행 결과
문자열의 길이: 12
문자열이 비어 있는지: false
문자열이 비어 있거나 공백인지 1: false
문자열이 비어 있거나 공백인지 2: true
7번째 인덱스의 문자 = J
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
package lang.string.method;
public class StringComparisonMain {
public static void main(String[] args) {
String str1 = "Hello, Java!"; // 대문자 일부 있음
String str2 = "hello, java!";
String str3 = "Hello, World!";
System.out.println("str equals str2: " + str1.equals(str2));
System.out.println("str equalsIgnoreCase str2: " + str1.equalsIgnoreCase(str2));
System.out.println("'a' compareTo 'b': " + "a".compareTo("b"));
System.out.println("'b' compareTo 'a': " + "b".compareTo("a"));
System.out.println("'c' compareTo 'a': " + "c".compareTo("a"));
System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2));
System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello"));
System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
}
}
실행 결과
str equals str2: false
str equalsIgnoreCase str2: true
'a' compareTo 'b': -1
'b' compareTo 'a': 1
'c' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
package lang.string.method;
public class StringSearchMain {
public static void main(String[] args) {
String str = "Hello, Java! Welcome to Java world.";
System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java"));
System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java"));
System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10));
System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java"));
}
}
실행 결과
문자열에 'Java'가 포함되어 있는지: true
'Java'의 첫 번째 인덱스: 7
인덱스 10부터 'Java'의 인덱스: 24
'Java'의 마지막 인덱스: 24
-
-
-
💾 [CS] ALU와 제어장치
ALU와 제어장치.
CPU: 메모리에 저장된 명령어를 읽어 들이고, 해석하고, 실행하는 장치
ALU: CPU 내부에 계산을 담당
레지스터: 명령어를 읽어 들이고 해석하는 제어장치, 작은 임시 저장 장치
ALU
ALU: 레지스터를 통해 피연산자 를 받아들이고, 제어장치로부터 수행할 연산을 알려주는 제어 신호 를 받아 들입니다.
레지스터와 제어장치로부터 받아들인 피연산자와 제어 신호로 산술 연산, 논리 연산 등 다양한 연산을 수행합니다.
ALU가 내보내는 정보.
연산을 수행한 결과는 특정 숫자나 문자가 될 수도 있고, 메모리 주소가 될 수도 있습니다.
그리고 이 결괏값은 바로 메모리에 저장되지 않고 일시적으로 레지스터에 저장됩니다.
CPU가 메모리에 접근하는 속도는 레지스터에 접근하는 속도보다 훨씬 느립니다.
ALU가 연산할 때마다 결과를 메모리에 저장한다면 당연하게도 CPU는 메모리에 자주 접근하게 되고, 이는 CPU가 프로그램 실행 속도를 늦출 수 있습니다.
그래서 ALU의 결괏값을 메모리가 아닌 레지스터에 우선 저장하는 것 입니다.
ALU는 계산 결과와 더불어 플래그를 내보냅니다.
ALU는 결괏값뿐만 아니라 연산 결과에 대한 추가적인 정보를 내보내야 할 때가 있습니다.
연산 결과에 대한 추가적인 상태 정보를 플래그(flag) 라고 합니다.
ALU가 내보내는 대표적인 플래그는 아래와 같습니다.
이러한 플래그는 CPU가 프로그램을 실행하는 도중 반드시 기억해야 하는 일종의 참고 정보입니다.
플래그들은 플래그 레지스터 라는 레지스터에 저장됩니다.
플래그 값들을 저장하는 레지스터입니다.
이 레지스터를 읽으면 연산 결과에 대한 추가적인 정보, 참고 정보를 얻을 수 있습니다.
플레그 레지스터 예시와 설명.
예를 들어 플래그 레지스터가 아래와 같은 구조를 가지고 있고, ALU가 연산을 수행한 직후 부호 플래그가 1이 되었다면 연산 결과는 음수임을 알 수 있습니다.
또한 만약 ALU가 연산을 수행한 직후 플래그 레지스터가 아래와 같다면 제로 플래그가 1이 되었으니 연산 결과는 0임을 알 수 있습니다.
이 밖에도 ALU 내부에는 여러 계산을 위한 회로들이 있습니다.
대표적으로
덧셈을 위한 가산기
뺄셈을 위한 보수기
시프트 연산을 수행해 주는 시프터
오버플로우를 대비한 오버플로우 검출기
등등
제어장치.
제어장치: 제어 신호를 내보내고, 해석하는 부품
제어 신호: 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호
제어장치가 받아들이는 정보.
첫째. 제어장치는 클럭 신호를 받아들입니다.
클럭(Clock): 컴퓨터의 모든 부품을 일사분란하게 움직일 수 있게하는 시간 단위
클럭의 주기에 맞춰 한 레지스터에서 다른 레지스터로 데이터가 이동되거나, ALU에서 연산이 수행되거나, CPU가 메모리에 저장된 명령어를 읽어 들어는 것 입니다.
다만, “컴퓨터의 모든 부품이 클럭 신호에 맞춰 작동한다” 라는 말을 “컴퓨터의 모든 부품이 한 클럭마다 작동한다”라고 이해하면 안됩니다.
컴퓨터 부품들은 클럭이라는 박자에 맞춰 작동할 뿐 한 박자마다 작동하는 건 아닙니다.
가령 다음 그림처럼 하나의 명령어가 여러 클럭에 걸쳐 실행될 수 있습니다.
둘째, 제어장치는 ‘해석해야 할 명령어’를 받아들입니다.
CPU가 해석해야 할 명령어는 명령어 레지스터 라는 특별한 레지스터에 저장됩니다.
제어장치는 이 명령어 레지스터로부터 해석할 명령어를 받아들이고 해석한 뒤, 제어 신호를 발생시켜 컴퓨터 부품들에 수행해야 할 내용을 알려줍니다.
셋째, 제어장치는 플래그 레지스터 속 플래그 값을 받아들입니다.
플래그는 ALU 연산에 대한 추가적인 상태 정보입니다.
제어장치는 플래그 값을 받아들이고 이를 참고하여 제어 신호를 발생 시킵니다.
넷째, 제어장치는 시스템 버스, 그중에서 제어 버스로 전달된 제어 신호를 받아들입니다.
제어 신호는 CPU뿐만 아니라 입출력장치를 비롯한 CPU 외부 장치도 발생시킬 수 있습니다.
제어장치는 제어 버스를 통해 외부로부터 전달된 제어 신호를 받아들이기도 합니다.
제어장치가 내보내는 정보.
여기에는 크게 CPU 외부에 전달하는 제어 신호와 CPU 내부에 전달하는 제어 신호가 있습니다.
제어장치가 CPU 외부에 제어 신호를 전달한다는 말은 곧, 제어 버스로 제어 신호를 내보낸다는 말과 같습니다.
이러한 제어 신호에는 크게 메모리에 전달하는 제어 신호와 입출력장치에 전달하는 제어 신호가 있습니다.
제어장치가 메모리에 저장된 값을 읽거나 메모리에 새로운 값을 쓰고 싶다면 메모리로 제어 신호를 내보냅니다.
그리고 제어장치가 입출력장치의 값을 읽거나 입출력장치에 새로운 값을 쓰고 싶을 때는 입출력장치로 제어 신호를 내보냅니다.
제어장치가 CPU 내부에 전달하는 제어 신호에는 크게 ALU에 전달하는 제어 신호와 레지스터에 전달하는 제어 신호가 있습니다.
ALU에는 수행할 연산을 지시하기 위해, 레지스터에는 레지스터 간에 데이터를 이동시키거나 레지스터에 저장된 명령어를 해석하기 위해 제어 신호를 내보냅니다.
키워드로 정리하는 핵심 포인트
ALU는 레지스터로부터 피연산자를 받아들이고, 제어장치로부터 제어 신호를 받아들입니다.
ALU는 연산 결과와 플래그를 내보냅니다.
제어장치는 클럭, 현재 수행할 명령어, 플래그, 제어 신호를 받아들입니다.
제어장치는 CPU 내부와 외보루 제어 신호 를 내보냅니다.
check point
이진수의 음수표현
2의 보수: 모든 0과 1을 뒤집고, 거기에 1을 더한 값
Q1. ALU가 소프트웨어 개발, 특히 iOS 개발에 어떻게 적용될 수 있는지 설명해 주세요. 예를 들어, 어떻게 ALU가 앱의 성능에 영향을 미칠 수 있는지 구체적인 사례를 들어주세요.
iOS 앱 개발에서 ALU의 역할은 직접적으로 보이지 않지만, 앱의 성능 최적화에 중요합니다. 예를 들어, 이미지 처리나 데이터 암호화 같은 작업은 많은 산술 및 논리 연산을 필요로 하며, 이는 ALU에서 처리됩니다. 따라서, ALU의 효율적인 사용은 앱의 반응 속도와 전반적인 성능에 직접적인 영향을 미칩니다.
Q2. ALU(산술 논리 장치)의 기본적인 기능은 무엇이며, 컴퓨터 프로세서 내에서 어떤 역할을 합니까?
ALU는 컴퓨터의 프로세서 내에 있는 하드웨어 구성 요소로, 기본적인 산술 연산(덧셈, 뺄셈, 곱셈, 나눗셈)과 논리 연산(AND, OR, XOR, NOT)을 수행합니다. 이는 모든 종류의 컴퓨터 프로그램 실행에 기본이 되는 연산이며, 프로세서가 복잡한 계산과 데이터 처리 작업을 수행할 수 있게 해줍니다.
Q3. Java 애플리케이션의 성능 최적화와 관련하여, ALU의 역할과 중요성에 대해 설명해 주세요.
Java 애플리케이션의 성능 최적화에서 ALU의 역할은 중요합니다. ALU는 계산 작업의 실제 수행 장소이므로, ALU의 효율성은 애플리케이션의 처리 속도와 직접적인 관련이 있습니다. 특히, 고성능을 요구하는 애플리케이션에서는 ALU를 통해 수행되는 연산의 최적화가 애플리케이션 전체의 성능을 크게 향상시킬 수 있습니다.
Q4. 멀티 쓰레딩 Java 애플리케이션에서 ALU의 처리 능력이 중요한 이유는 무엇이라고 생각하나요?
멀티 쓰레딩 애플리케이션에서는 여러 쓰레드가 동시에 연산을 수행할 수 있으므로, ALU의 처리 능력이 성능의 병목 현상을 방지하는 데 중요합니다. 효율적인 ALU 설계는 복수의 연산을 동시에 빠르게 처리할 수 있게 해주며, 이는 멀티 쓰레딩 환경에서 애플리케이션의 반응 속도와 처리량을 크게 향상시킬 수 있습니다.
Q5. 현대의 CPU가 여러 ALU를 갖고 있는 경우, 이것이 Java 백엔드 시스템의 성능에 어떤 영향을 미칠 수 있나요?
여러 ALU를 갖는 프로세서는 동시에 여러 연산을 수행할 수 있으므로, Java 백엔드 시스템에서의 병렬 처리 능력을 크게 향상시킵니다. 이는 데이터베이스 쿼리 처리, 대규모 데이터 분석, 실시간 트랜잭션 처리 등 다양한 작업에서 성능 이점을 제공할 수 있습니다.
Q6. Java 애플리케이션에서 복잡한 수학적 연산을 효율적으로 처리하기 위해 개발자가 고려해야 할 ALU와 관련된 측면은 무엇인가요?
개발자는 복잡한 수학적 연산을 효율적으로 처리하기 위해, ALU의 연산 처리 능력을 최대화하는 방법을 고려해야 합니다. 이는 알고리즘의 최적화, 복잡한 연산의 분할 및 정복 전략 적용, 필요한 경우 하드웨어 가속기(예: GPU) 사용 등을 포함할 수 있습니다.
Q7. ALU의 한계를 넘어서서 Java 애플리케이션의 성능을 향상시키기 위해 사용할 수 있는 다른 하드웨어 기반 최적화 기술은 무엇이 있을까요?
ALU의 한계를 넘어서 Java 애플리케이션의 성능을 향상시키기 위해, 다중 코어 프로세싱, 병렬 처리, GPU 가속, FPGA(필드 프로그래밍 게이트 어레이)를 활용한 커스텀 하드웨어 가속 등의 기술을 활용할 수 있습니다. 이러한 기술들은 특정 유형의 작업에 대해 상당한 성능 향상을 제공할 수 있습니다.
-
☕️[Java] String 클래스 - 기본
String 클래스 - 기본.
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
package lang.string;
public class CharArrayMain {
public static void main(String[] args) {
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
System.out.println(charArr);
String str = "hello";
System.out.println("str = " + str);
}
}
실행 결과
hello
str = hello
기본형인 char는 문자 하나를 다룰 때 사용합니다.
char를 사용해서 여러 문자를 나열하려면 char[]을 사용해야 합니다.
하지만 이렇게 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다.
package lang.string;
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
쌍따옴표: "hello"
객체 생성: new String("hello");
String은 클래스입니다.
int, boolean 같은 기본형이 아니라 참조형입니다.
따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다.
따라서 다음 코드는 뭔가 어색합니다.
String str1 = "hello";
문자열은 매우 자주 사용됩니다.
그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해 줍니다.(이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.)
String str1 = "hello"; // 기존
String str1 = new String("hello"); // 변경
String 클래스 구조
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
클래스이므로 속성과 기능을 가집니다.
속성(필드)
private final char[] value;
여기에는 String의 실제 문자열 값이 보관됩니다.
문자 데이터 자체는 char[] 에 보관됩니다.
String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다.
그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다.
참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다.
그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다.
그래서 단순 영어, 숫자로 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용)
그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다.
따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다.
기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다.
기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아봅시다.
주요 메서드는 다음과 같습니다.
length() : 문자열의 길이를 반환합니다.
charAt(inte index) : 특정 인덱스의 문자를 반환합니다.
substring(int beinIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOF(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
String 클래스와 참조형
String은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값들이 들어있는 것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
```java
package lang.string;
public class StringConcatMain {
public static void main(String[] args) {
String a = “hello”;
String b = “ jave”;
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2); } } ``` - 자바에서 문자열을 더할 때는 `String`이 제공하는 `concat()`과 같은 메서드를 사용해야 합니다.
- 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 `+` 연산을 제공합니다.
실행 결과
result1 = hello jave
result2 = hello jave
-
-
-
💉[SQL] REPLACE, SUBSTRING, CONCAT
REPLACE
‘REPLACE’ 함수는 SQL에서 문자열 내의 특정 부분을 다른 문자열로 바꾸고자 할 때 사용됩니다.
이 함수는 데이터 정제나 수정 작업에서 특히 유용하며, 기존 문자열 내의 특정 패턴이나 문자를 찾아 이를 새로운 문자열로 대체하는 기능을 제공합니다.
‘REPLACE’ 는 로그 데이터 정리, 사용자 입력 데이터의 표준화, 데이터 마이그레이션 작업 등 다양한 상황에서 활용될 수 있습니다.
‘REPLACE’ 사용 예
특정 문자열 대체: 고객 데이터에서 전화번호 형식을 변경하고 싶을 때
SELECT REPLACE(phone_number, '-', '') FROM customers;
이 쿼리는 ‘customers’ 테이블의 ‘phone_number’ 열에서 모든 ’-‘ 를 제거합니다.
예를 들어, ‘123-456-7890’ 이라는 전화번호가 있을 경우, ‘1234567890’ 으로 변경됩니다.
데이터 정제: 사용자의 이메일 주소에서 도메인을 변경하고 싶을 때
UPDATE users SET email = REPLACE(email, '@old_domain.com', '@new_domain.com');
이 쿼리는 ‘users’ 테이블의 ‘email’ 열에서 ‘@old_domail.com’ 을 ‘@new_domain.com’ 으로 변경합니다.
텍스트 내용 수정: 상품 설명에서 특정 단어를 새로운 단어로 바꾸고 싶을 때
UPDATE products SET description = REPLACE(description, 'oldword', 'newword');
이 쿼리는 ‘products’ 테이블의 ‘description’ 열에서 ‘oldword’ 를 ‘newword’ 로 변경합니다.
‘REPLACE’ 함수의 특징.
‘REPLACE’ 는 대소문자를 구분하여 작동합니다.
대소문자 구분 없이 대체를 하고자 할 경우, 추가적인 함수나 조건을 사용해야 할 수 있습니다.
문자열 내에서 지정된 패턴이나 문자열을 찾아 모두 대체합니다.
찾고자 하는 문자열이 존재하지 않으면, 원본 문자열이 변경 없이 그대로 반환됩니다.
‘REPLACE’ 함수는 ‘SELECT’, ‘UPDATE’ 등의 쿼리 내에서 사용할 수 있으며, 데이터 조회 또는 수정 작업에 모두 적용할 수 있습니다.
사용 시 고려사항
대량의 데이터를 처리할 때는 ‘REPLACE’ 함수를 사용하는 쿼리의 성능에 주의해야 합니다.
특히 ‘UPDATE’ 작업에서는 대체 작업으로 인해 대량의 데이터가 변경될 수 있으므로, 사전에 작업 범위를 잘 파악하고 필요한 백업을 수행하는 것이 좋습니다.
문자열 대체 작업을 수행할 때는 원치 않는 데이터 변경을 방지하기 위해, 대체할 문자열이 정확히 일치하는지 사전에 확인하는 것이 중요합니다.
‘REPLACE’ 함수는 문자열 데이터를 쉽게 수정하고 정제할 수 있는 강력한 도구로, 데이터베이스 내의 데이터 관리 및 유지보수 작업에 널리 사용됩니다.
SUBSTRING
‘SUBSTRING’ 함수는 SQL에서 문자열의 특정 부분을 추출할 때 사용됩니다.
이 함수는 문자열 데이터 내에서 특정 위치를 기준으로 한 부분 문자열(substring)을 반환하며, 데이터 정제, 특정 형식의 데이터 추출, 또는 문자열 처리 작업에서 매우 유용합니다.
‘SUBSTRING’ 은 로그 분석, 데이터 마이그레이션, 사용자 입력의 특정 부분 처리 등 다양한 상황에서 활용될 수 있습니다.
‘SUBSTRING’ 사용 예
특정 위치의 문자열 추출: 사용자 이메일에서 도메인 부분만을 추출하고 싶을 때
SELECT SUBSTRING(email FROM POSITION ('@' IN email) + 1) FROM users;
이 쿼리는 ‘users’ 테이블의 ‘email’ 열에서 ’@’ 기호 뒤의 도메인 부분을 추출합니다.
고정된 형식의 문자열 처리: 전화번호에서 지역 코드를 추출하고 싶을 때
SELECT SUBSTRING(phone_number, 1, 3) FROM customers;
이 쿼리는 ‘customers’ 테이블의 ‘phone_number’ 열에서 처음 3자리(지역 코드)를 추출합니다.
문자열의 특정 부분 수정 작업에 사용: 주소에서 특정 부분을 다른 형식으로 변경하고 싶을 때
UPDATE addresses SET street = SUBSTRING(street, 1, 10) || '...' WHERE LENGTH(street) > 10;
이 쿼리는 ‘addresses’ 테이블의 ‘street’ 열에서 문자열의 길이가 10자를 초과하는 경우, 처음 10자만을 남기고 그 뒤를 ‘…‘ 으로 대체합니다.
‘SUBSTRING’ 함수의 특징
‘SUBSTRING’ 은 문자열의 특정 섹션을 반환하는 데 사용되며, 시작 위치와 길이(선택적)를 지정하여 원하는 부분 문자열을 추출할 수 있습니다.
다양한 문자열 처리 작업에 활용될 수 있으며, 데이터의 형식을 변경하거나, 특정 패턴에 기반한 정보를 추출하는 등의 목적으로 사용됩니다.
함수의 정확한 구문은 사용하는 SQL 데이터베이스 시스템에 따라 약간씩 다를 수 있으므로, 해당 시스템의 문서를 참조하는 것이 좋습니다.
사용 시 고려사항
‘SUBSTRING’ 함수를 사용할 때는 문자열의 인덱스가 1부터 시작한다는 점을 주의해야 합니다.(대부분의 SQL 시스템에서).
대량의 데이터를 처리할 때는 ‘SUBSTRING’ 함수를 사용하는 쿼리의 성능에 주의해야 합니다. 필요한 경우, 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘SUBSTRING’ 함수는 문자열 데이터를 효과적으로 처리하고 분석하는 데 있어 필수적인 도구로, 데이터베이스 내에서 다양한 문자열 조작 작업을 수행하는 데 널리 사용됩니다.
CONCAT
‘CONCAT’ 함수는 SQL에서 두 개 이상의 문자열을 하나로 결합할 때 사용됩니다.
이 함수는 데이터베이스 내에서 다양한 문자열 정보를 합쳐 새로운 문자열 값을 생성하고자 할 때 유용하며, 보고서 작성, 데이터 형식의 표준화, 사용자 이름이나 주소와 같은 데이터의 결합 등 다양한 상황에서 활용될 수 있습니다.
‘CONCAT’ 사용 예
단순한 문자열 결합: 사용자의 이름과 성을 하나의 문자열로 결합하고 싶을 때
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
이 쿼리는 ‘users’ 테이블의 ‘first_name’ 과 ‘last_name’ 을 공백으로 구분하여 결합한 후, ‘full_name’ 이라는 새로운 열로 결과를 반환합니다.
복수의 열 결합: 고객의 주소 정보를 하나의 문자열로 결합하고 싶을 때
SELECT CONCAT(street_address, ', ', city, state, ' ', postal_code) AS full_address FROM customers;
이 쿼리는 ‘customers’ 테이블에서 여러 주소 관련 열을 콤마와 공백을 사용하여 결합하고, 이를 ‘full_address’ 라는 새로운 열로 결과를 반환합니다.
데이터 형식 표준화: 상품 코드와 상품 이름을 결합하여 표준 형식의 상품 정보를 생성하고 싶을 떄
SELECT CONCAT(product_code, ': ', product_name) AS product_info FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_code’ 와 ‘product_name’ 을 콜론과 공백으로 구분하여 결합한 후,
‘product_info’ 라는 새로운 열로 결과를 반환합니다.
‘CONCAT’ 함수의 특징
‘CONCAT’ 함수는 두 개 이상의 문자열을 매개변수로 받아 이들을 순서대로 결합한 새로운 문자열을 생성합니다.
거의 모든 SQL 데이터베이스 시스템에서 지원되며, 문자열 처리와 데이터 형식의 변환에 널리 사용됩니다.
일부 데이터베이스 시스템에서는 ‘CONCAT’ 대신 연산자(**’
‘** 등)를 사용하여 문자열을 결합할 수도 있습니다.
사용 시 고려사항
결합하려는 문자열 중 하나라도 ‘NULL’ 값을 포함하는 경우, ‘CONCAT’ 의 동작은 데이터베이스 시스템에 따라 다를 수 있습니다.
예를 들어, 일부 시스템은 ‘NULL’ 을 빈 문자열로 취급할 수 있으나, 다른 시스템에서는 전체 결과가 ‘NULL’ 이 될 수 있습니다.
복잡한 문자열 결합을 수행할 때는 성능에 주의해야 하며, 특히 대량의 데이터를 처리할 때는 쿼리 성능을 테스트하고 최적화하는 것이 중요합니다.
‘CONCAT’ 함수는 문자열 데이터를 결합하고 조작하는 과정에서 필수적인 도구로, 데이터베이스 내에서 다양한 문자열 관련 작업을 구행하는 데 활용됩니다.
-
☕️[Java] 불변 객체 - 예제
불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
앞의 Address, ImmutableAddress를 그래로 활용합니다.
변경 클래스 사용
package lang.immutable.address;
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원 B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원A와 회원B는 둘다 서울에 살고 있습니다.
중간에 회원B의 주소를 부산으로 변경해야 합니다.
그런데 회원A와 회원B는 같은 Address 인스턴스를 참조하고 있습니다.
회원B의 주소를 부산으로 변경하는 순간 회원A의 주소도 부산으로 변경됩니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
package lang.immutable.address;
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
//memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원B의 주소를 중간에 부산으로 변경하려고 시도합니다.
하지만 ImmutableAddress에는 값을 변경할 수 있는 메서드가 없습니다.
따라서 컴파일 오류가 발생합니다.
결국 memberB.setAddress(new ImmutableAddress("부산"))와 같이 새로운 주소 객체를 만들어서 전달합니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
사이드 이펙트가 발생하지 않습니다. 회원A는 기존 주소를 그대로 유지합니다.
-
💾 [CS] 명령어의 구조
명령어의 구조
연산코드와 오퍼랜드
아래 그림을 보면 색 배경 필드는 명령의 ‘작동’, 달리 말해 ‘연산’을 담고 있고 흰색 배경 필드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 담고 있습니다.
명령어 : 연산 코드와 오퍼랜드로 구성되어 있습니다.
연산코드(Opreation Code): 색 배경 필드 값, 즉 ‘명령어가 수행할 연산’을 연산코드(Operation Code) 라 합니다.
오퍼랜드(Operand) : 흰색 배경 필드 값, 즉 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 오퍼랜드라고 합니다.
연산 코드는 연산자, 오퍼랜드는 피연산자 라고도 부릅니다.
연산 코드 필드: 연산 코드가 담기는 영역(색칠된 부분)
오퍼랜드 필드: 오퍼랜드가 담기는 영역(색칠되지 않은 부분)
오퍼랜드
오퍼랜드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 의미합니다.
그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있습니다.
다만 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접 명시하기보다는, 많은 경우 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담깁니다.
그래서 오퍼랜드 필드를 주소 필드 라고 부르기도 합니다.
오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러개가 있을 수도 있습니다.
오퍼랜드가 하나도 없는 명령어 0-주소 명령어
오퍼랜드가 하나인 명령어 1-주소 명령어
오퍼랜드가 두 개인 명령어 2-주소 명령어
오퍼랜드가 세 개인 명령어 3-주소 명령어
연산 코드
연산 코드 종류는 매우 많지만, 가장 기본적인 연산 코드 유형은 크게 네 가지로 나눌 수 있습니다.
데이터 전송
산술/논리 연산
제어 흐름 변경
입출력 제어
주소 지정 방식
연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address) 라고 합니다.
오퍼랜드 필드에 데이터가 저장된 위피를 명시 할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식(addressing mode) 이라고 합니다
다시 말해, 주소 지정 방식은 유효 주소를 찾는 방법입니다.
즉시 주소 지정 방식
즉시 주소 지정 방식(immediate addressing mode): 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식입니다.
이런 방식은 표현할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 이하 설명할 주소 지정 방식들보다 빠릅니다.
직접 주소 지정 방식
직접 주소 지정 방식(direct addressing mode): 오퍼랜드 필드에 유효 주소를 직접 명시하는 방식입니다.
오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 더 커졌지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었습니다.
다시 말해 표현할 수 있는 오퍼랜드 필드의 길이가 연산 코드의 길이만큼 짧아져 표현할 수 있는 유효 주소에 제한이 생길 수 있습니다.
간접 주소 지정 방식
간접 주소 지정 방식(indirect addressing mode): 유효 주소의 주소를 오퍼랜드 필드에 명시합니다. 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓습니다.
두 번의 메모리 접근이 필요하기 때문에 앞서 설명한 주소 지정 방식들보다 일반적으로 느린 방식입니다.
레지스터 주소 지정 방식
레지스터 주소 지정 방식(register addressing mode): 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법입니다.
일반적으로 CPU 외부에 있는 메모리에 접근하는 것보다 CPU 내부에 있는 레지스터에 접근하는 것이 더 빠릅니다.
그러므로 레지스터 주소 지정 방식은 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있습니다.
다만, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유합니다. 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다는 점입니다.
레지스터 간접 주소 지정 방식
레지스터 간접 주소 지정 방식(register indirect addressing mode): 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법입니다.
유효 주소를 찾는 과정이 간전 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있습니다.
레지스터 간접 주소 지장 방식은 간접 주소 지정 방식보다 빠릅니다.
정리
연산에 사용할 데이터를 찾는 방법을 주소 지정 방식 이라고 했습니다.
연산에 사용할 데이터가 저장된 위치를 유효 주소 라고 했습니다.
대표적인 주소 지정 방식으로 아래의 다섯 가지 방식을 소개했습니다.
각각의 방식이 오퍼랜드 필드에 명시하는 값을 정리해 보면 아래와 같습니다.
즉시 주소 지정 방식: 연산에 사용할 데이터
직접 주소 지정 방식: 유효 주소(메모리 주소)
간접 주소 지정 방식: 유효 주소의 주소
레지스터 주소 지정 방식: 유효 주소(레지스터 이름)
레지스터 간접 주소 지정 방식: 유효 주소를 저장한 레지스터
키워드로 정리하는 핵심 포인트
명령어 는 연산 코드와 오퍼랜드로 구성됩니다.
연산 코드는 명령어가 수행할 연산을 의미합니다.
오퍼랜드는 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치를 의미합니다.
주소 지정 방식은 연산에 사용할 데이터 위치를 찾는 방법입니다.
Q1. Swift에서 메모리 주소에 접근하기 위해 어떤 타입을 사용할 수 있는지 설명해 주세요. 그리고 왜 이러한 접근 방식이 필요할까요?
Swift에서 메모리 주소에 직접 접근하기 위해 UnsafePointer<T> 타입과 그 변형인 UnsafeMutablePointer<T>를 사용할 수 있습니다. 이러한 포인터들은 C 언어의 포인터와 유사하게 작동하며, 메모리의 특정 위치를 직접 가리키는 데 사용됩니다. 이러한 접근 방식은 일반적으로 Swift의 안전성 및 추상화 원칙에 어긋나지만, 성능 최적화, 기존 C 기반 코드와의 상호 작용, 혹은 저수준 시스템 인터페이스와의 직접적인 상호 작용이 필요한 경우에 필요할 수 있습니다. 예를 들어, 대량의 데이터 처리나 기존 C 라이브러리의 함수를 호출할 때 이러한 방식이 유용할 수 있습니다.
아래는 주니어 Java 백엔드 개발자 면접 질문에 대한 모범 답안 예시입니다. 이 답변들은 Java의 메모리 관리와 관련된 기본적인 지식을 보여주는 데 목적이 있습니다.
Q2. Java에서는 일반적으로 개발자가 직접 메모리 주소를 다루지 않습니다. 이에 대한 이유를 설명해 주세요. 또한, 자동 메모리 관리는 어떤 장점을 제공하나요?
답변: Java에서 개발자가 직접 메모리 주소를 다루지 않는 주된 이유는 Java가 자동 메모리 관리 시스템인 가비지 컬렉션(Garbage Collection, GC)을 제공하기 때문입니다. 이로 인해 메모리 누수와 같은 오류를 방지하고, 개발자가 메모리 관리에 드는 시간과 노력을 줄일 수 있습니다. 자동 메모리 관리의 장점으로는 안정성의 향상, 메모리 관리 오류의 감소, 그리고 개발자의 생산성 향상 등이 있습니다.
Q3. JVM의 메모리 모델을 설명해 주세요. Heap과 Stack 메모리 영역의 차이점은 무엇이며, 각각 어떤 종류의 데이터를 저장하나요?
답변: JVM의 메모리 모델은 크게 Heap 영역과 Stack 영역으로 나뉩니다. Heap 영역은 모든 스레드에 걸쳐 공유되며, 주로 객체와 클래스의 메타데이터가 저장됩니다. 가비지 컬렉션은 이 Heap 영역에서 주로 작동합니다. 반면, Stack 영역은 스레드 별로 별도로 할당되며, 메소드 호출과 관련된 지역 변수와 참조 변수를 저장합니다. Stack은 LIFO(Last In, First Out) 방식으로 데이터를 관리합니다.
Q4. 대규모 데이터 처리 작업을 수행할 때 Java에서 메모리 효율을 최적화하는 방법에는 어떤 것들이 있나요?
답변: 대규모 데이터 처리 시 메모리 효율을 최적화하기 위해, 객체 재사용, 적절한 컬렉션 선택, 스트림 API 사용, 그리고 메모리 캐싱 전략 등을 적용할 수 있습니다. 예를 들어, 객체 풀링을 통해 빈번히 생성 및 파괴되는 객체의 생성 비용을 줄일 수 있습니다. 또한, 데이터 양에 따라 적절한 자료구조를 선택하여 메모리 사용량과 성능을 균형있게 관리할 수 있습니다.
Q5. JNI(Java Native Interface)는 무엇이며, 왜 사용하나요? Java 애플리케이션에서 JNI를 사용하여 네이티브 코드와 상호 작용하는 예를 들 수 있나요?
답변: JNI(Java Native Interface)는 Java 코드 내에서 C나 C++과 같은 네이티브 코드를 호출하거나, 반대로 네이티브 코드에서 Java 코드를 호출할 수 있는 프로그래밍 프레임워크입니다. JNI는 시스템 레벨의 리소스나 레거시 라이브러리를 사용해야 할 때, 또는 성능상의 이유로 직접 하드웨어를 제어해야 할 때 사용됩니다. 예를 들어,
고성능 그래픽 처리나 특정 하드웨어 장치와의 직접적인 상호작용을 구현할 때 JNI를 사용할 수 있습니다.
Q6. 가비지 컬렉션(Garbage Collection)의 기본 원리를 설명해 주세요. Java에서 가비지 컬렉터의 작동 방식에 영향을 미칠 수 있는 프로그래밍 관행에는 어떤 것들이 있나요?
답변: 가비지 컬렉션은 참조되지 않는 객체를 자동으로 검출하고, 이를 메모리에서 제거하여 메모리를 회수하는 프로세스입니다. Java에서 가비지 컬렉터의 효율성에 영향을 미칠 수 있는 프로그래밍 관행으로는, 객체 참조를 적절히 해제하는 것, 대용량 객체의 재사용, 그리고 적절한 컬렉션 사용 등이 있습니다. 불필요한 객체 참조를 남겨두지 않고, 메모리 사용량이 큰 객체는 풀링 기법을 사용하여 관리함으로써, 가비지 컬렉터의 부하를 줄이고 애플리케이션의 성능을 개선할 수 있습니다.
-
☕️[Java] 공유 참조와 사이드 이펙트
공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 b의 값을 부산으로 변경한 코드를 다시 분석해 봅시다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 a, b는 같은 인스턴스를 참조합니다. 따라서 a의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라고 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
사이드 이펙트 해결방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 a와 b가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
코드를 작성해봅시다.
package lang.immutable.address;
public class RefMain1_2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = new Address("서울");
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 b의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
a와 b는 서로 다른 Address 인스턴스를 참조합니다.
a와 b는 서로 다른 인스턴스를 참조합니다.
따라서 b가 참조하는 인스턴스의 값을 변경해도 a에는 영향을 주지 않습니다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 a,b가 서로 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유
Address a = new Address("서울");
Address b = a;
이 경우 a, b 둘 다 같은 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유 하지 않음
Address a = new Address("서울");
Address b = new Address("서울");
이 경우 a, b는 서로 다른 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 가능합니다.
이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다.
쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다.
그런데 여기에 문제가 있습니다.
하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다
다음 예시를 봅시다.
참조값의 공유를 막을 수 있는 방법이 없습니다.
Address a = new Address("서울");
Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다.
b = a와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다.
하지만 Address를 사용하는 개발자 입장에서 실수로 b = a라고 해도 아무런 오류가 발생하지 않습니다.
왜냐하면 자바 문법상 Address b = a와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다.
Address b = new Address("서울"); // 새로운 객체 참조
Address b = a // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
쉽게 이야기해서 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 눈을 크게 잘 뜨고! 집중해서 코드를 잘 작성하면서 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
다음 코드를 봅시다.
package lang.immutable.address;
public class RefMain1_3 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
change(b, "부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
private static void change(Address address, String changeAddress) {
System.out.println("주소 값을 변경합니다 -> " + changeAddress);
address.setValue(changeAddress);
}
}
앞서 작성한 코드와 같은 코드입니다.
단순히 change() 메서드만 하나 추가되었습니다.
그리고 change() 메서드에서 Address 인스턴스에 있는 value 값을 변경합니다.
main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이류를 찾기가 더 어렵습니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}
여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법은 없습니다.
그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까요?
단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까요?
-
☕️[Java] 불변 객체 - 도입
불변 객체 - 도입
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제입니다.
하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없습니다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
앞의 예를 떠올려보면 a, b는 처음 시점에는 둘 다 "서울"이라는 주소를 사용해야 합니다.
그리고 이후에 b의 주소를 "부산"으로 변경해야 합니다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 b = a와 같이 "서울"이라는 Address 인스턴스를 a, b가 함께 사용하는 것이, 다음 코드와 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다.
인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 Address b = a와 같이 공유 참조를 사용해도 아무런 문제가 없습니다. 오히려 더 효율적입니다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다.
b.setValue("부산"); // b의 값을 부산으로 변경해야 합니다.
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다.
기본형과 다르게 참조형인 객체는 처음부터 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다.
따라서 이것은 문제가 아닙니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 더이선가 변경했기 때문입니다.
만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
불변 객체 도입
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어 봅시다.
package lang.immutable.address;
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
내부 값이 변경되면 안됩니다.
따라서 value의 필드를 final로 선언했습니다.
값을 변경할 수 있는 setValue()를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
package lang.immutable.address;
public class RefMain2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다.
System.out.println("a = " + a);
System.out.println("b = " + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
}
}
ImmutableAddress의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었습니다.
이제 ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없습니다.
ImmutableAddress를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫습니다.
예를 들어 b.setValue("부산")을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다.
따라서 어쩔 수 없이 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b에 대입한다.
결과적으로 a, b는 서로 다른 인스턴스를 참조하고, a가 참조하던 ImmutableAddress는 그대로 유지됩니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 a의 값은 그대로 유지되는 것을 확인할 수 있습니다.
자바에서 객체의 공유 참조는 막을 수 없습니다.
ImmutableAddress는 불변 객체입니다. 따라서 값을 변경할 수 없습니다.
ImmutableAddress은 불변 객체이므로 b가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
참고 - 가변(Mutable) 객체 VS 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.)
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.)
Address 는 가변 클래스입니다. 이 클래스로 객체를 생성하면 가변 객체가 됩니다.
ImmutableAddress는 불변 클래스입니다. 이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
💉[SQL] ORDER BY
ORDER BY
‘ORDER BY’ 절은 SQL 쿼리의 결과를 특정 기준에 따라 정렬할 때 사용됩니다.
이를 통해 반환된 데이터를 오름차순(ASC) 또는 내림차순(DESC)으로 정렬할 수 있으며, 숫자, 문자열, 날짜 등 다양한 데이터 타입에 적용할 수 있습니다.
‘ORDER BY’ 는 데이터를 보다 읽기 쉽고 분석하기 용이하게 정렬하여 제공함으로써, 데이터 리포팅, 사용자 인터페이스에서의 데이터 표시, 데이터 분석 등 다양한 상황에서 유용하게 사용됩니다.
‘OREDER BY’ 사용 예
특정 열에 따른 오름차순 정렬: 직원들을 이름순으로 정렬하고 싶을 때
SELECT * FROM emploees ORDER BY name ASC;
이 쿼리는 ‘employees’ 테이블의 모든 행을 ‘name’ 열 기준으로 오름차순으로 정렬하여 반환합니다.
특정 열에 따른 내림차순 정렬: 최신 주문부터 표시하고 싶을 때
SELECT * FROM orders ORDER BY order_by DESC;
이 쿼리는 ‘orders’ 테이블의 모든 행을 ‘order_date’ 열 기준으로 내림차순으로 정렬하여 반환합니다.
여러 열에 따른 정렬: 부서별로 그룹화하고, 각 부서 내에서 급여가 높은 순으로 정렬하고 싶을 때
SELECT * FROM employees ORDER BY department ASC, salary DESC;
이 쿼리는 먼저 ‘department’ 열로 오름차순으로 정렬하고, 같은 부서 내에서는 ‘salary’ 열을 기준으로 내림차순으로 정렬합니다.
‘ORDER BY’ 절의 특징
기본적으로 ‘ORDER BY’ 는 오름차순(ASC)으로 정렬합니다. 내림차순으로 정렬하고 싶다면 각 열 이름 뒤에 ‘DESC’ 키워드를 명시해야 합니다.
여러 열을 기준으로 정렬할 수 있으며, 이 경우 첫 번째 열을 기준으로 정렬한 후 동일한 값에 대해서는 다음 열의 순서에 따라 정렬합니다.
‘SELECT’ 쿼리의 마지막 부분에 위치하며, ‘WHERE’, ‘GROUP BY’, ‘HAVING’ 절 뒤에 명시됩니다.
사용 시 고려사항
‘ORDER BY’ 를 사용할 때는 정렬하고자 하는 열이 인덱싱되어 있는지 확인하는 것이 좋습니다. 특히 대규모 데이터셋을 다룰 때, 인덱스의 유무는 쿼리 성능에 큰 영향을 미칩니다.
복잡한 쿼리에서는 ‘ORDER BY’ 로 인한 추가적인 처리 시간이 필요할 수 있으므로, 성능과 관련하여 적절한 테스트가 필요합니다.
‘ORDER BY’ 절은 SQL 쿼리의 결과를 사용자가 원하는 순서로 쉽게 정렬할 수 있게 해주며, 데이터의 가독성과 분석의 용이성을 크게 향상시킵니다.
-
💉[SQL] GROUP BY
GROUP BY
‘GROUP BY’ 절은 SQL에서 특정 열(들)의 값에 기반하여 행(row)들을 그룹화할 때 사용됩니다.
이 기능은 집계함수(‘SUM’, ‘AVG’, ‘COUNT’, ‘MIN’, ‘MAX’ 등)와 함꼐 사용되어, 각 그룹에 대한 집계된 데이터를 계산하고 반환하는 데 주로 활용됩니다.
‘GROUP BY’ 는 데이터를 요약하고, 특정 기준에 따른 데이터의 통계를 분석할 때 유용하게 사용됩니다.
‘GROUP BY’ 사용 예
그룹별 합계 계산: 각 부서별 총 급여를 계산하고 싶을 때
SELECT department, SUM(salary) FROM employees GROUP BY departmentl
이 쿼리는 ‘employees’ 테이블에서 ‘department’ 별로 그룹화하고, 각 그룹의 ‘salary’ 합계를 계산합니다.
그룹별 평균 계산: 각 제품 카테고리별 평균 가격을 계산하고 싶을 때
SELECT category, AVG(price) FROM products GROUP BY category;
이 쿼리는 ‘product’ 테이블에서 ‘category’ 별로 그룹화하고, 각 그룹의 price 평균을 계산합니다.
그룹별 데이터 수 계산: 각 부서에 속한 직원 수를 세고 싶을 때
SELECT department, COUNT(*) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 ‘department’ 별로 그룹화하고, 각 그룹의 직원 수를 세어 반환합니다.
‘GROUP BY’ 절의 특징
데이터를 그룹화하고 각 그룹에 대한 집계를 수행하여, 데이터의 요약 정보를 제공합니다.
여러 열을 기준으로 그룹화할 수 있으며, 이 경우 선택된 모든 열의 조합에 따라 데이터가 그룹화됩니다.
집계 함수와 함께 사용되어, 각 그룹별로 함계, 평균, 최소값, 최대값 등을 계산할 수 있습니다.
‘HAVING’ 절과 함께 사용하여, 집계 결과에 대한 조건을 설정할 수 있습니다. 이는 ‘WHERE’ 절과 유사하지만, ‘GROUP BY’ 로 그룹화된 결과에 대해 조건을 정용하는 점이 다릅니다.
사용 시 고려사항
‘GROUP BY’ 를 사용할 때는 선택된 열이 ‘SELECT’ 절에 포함되어야 합니다. 그렇지 않은 경우, SQL 쿼리가 예상대로 작동하지 않을 수 있습니다.
대규모 데이터셋에서 ‘GROUP BY’ 를 사용할 때는 쿼리의 성능을 고려해야 합니다. 적절한 인덱스 사용과 데이터 구조의 최적화가 성능에 큰 영햫을 미칠 수 있습니다.
‘GROUP BY’ 절은 데이터를 분석하고 요약 정보를 얻기 위한 강력한 도구로, 데이터베이스 내에서 의미 있는 인사이트를 도출하는 데 크게 기여합니다.
-
-
-
💉[SQL] SUM, AVG, COUNT, MIN, MAX
SUM
‘SUM’ 함수는 SQL에서 특정 열(column)에 포함된 숫자 값들의 합계를 계산할 때 사용됩니다.
이는 집계 함수의 한 종류로, 주로 ‘GROUP BY’ 절과 함꼐 사용되어 여러 그룹의 데이터에 대한 합계를 구하거나, 전체 테이블에서 특정 열의 총합을 계산하는 데 적용됩니다.
‘SUM’ 함수는 보고서 생성, 데이터 분석, 재무 계산 등 다양한 상황에서 유용하게 활용될 수 있습니다.
‘SUM’ 사용 예
전체 합계 계산 : 모든 주문의 총 금액을 계산하고 싶을 때
SELECT SUM(total_price) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘total_price’ 열에 있는 모든 값의 합계를 반환합니다.
그룹별 합계 계산 : 각 부서별 직원들의 총 급여를 계산하고 싶을 떄
SELECT department, SUM(salary) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 각 ‘department’ 별로 ‘salary’ 열의 합계를 계산하여, 각 부서의 총 급여를 보여줍니다.
조건부 합계 계산 : 2023년에 이루어진 모든 판매의 총액을 계산하고 싶을 때
SELECT SUM(sales_amount) FROM sales WHERE year = 2023;
이 쿼리는 ‘sales’ 테이블에서 ‘year’ 열이 2023인 모든 행의 ‘sales_amount’ 열 값의 합계를 반환합니다.
‘SUM’ 함수의 특징
‘SUM’ 함수는 숫자 데이터에 대해서만 사용할 수 있으며, 문자열이나 날짜 등의 데이터 타입에는 사용할 수 없습니다.
‘NULL’ 값을 포함하는 열에 ‘SUM’ 함수를 사용할 때, ‘NULL’ 값은 0으로 간주되지 않고, 단순히 무시됩니다.
즉, ‘NULL’ 값은 합계 계산에 영향을 주지 않습니다.
‘SUM’ 은 다른 집계 함수(‘COUNT’, ‘AVG’, ‘MIN’, ‘MAX’ 등)와 함께 사용될 수 있으며, 복잡한 데이터 집합에 대한 요약 정보를 제공하는 데 유용합니다.
사용 시 고려사항
‘SUM’ 함수를 사용할 때는 대상 열이 숫자 타입임을 확인해야 합니다.
큰 데이터 세트에서 ‘SUM’ 함수를 사용할 때는 쿼리 성능에 주의해야 합니다.
필요한 경우 적절한 인덱스를 사용하여 성능을 최적화할 수 있습니다.
‘GROUP BY’ 절과 함께 ‘SUM’ 을 사용할 때는, 그룹화할 열을 명확히 지정해야 합니다.
‘SUM’ 함수는 데이터베이스에서 숫자 데이터의 합계를 계산하는 데 매우 중요한 도구로, 데이터 분석 및 보고서 작성 등 다양한 상황에서 활용될 수 있습니다.
AVG
‘AVG’ 함수는 SQL에서 특정 열(column)에 포함된 숫자 값들의 평균을 계산할 때 사용됩니다.
이 집계 함수는 특정 데이터 집합의 중간 값을 찾거나, 데이터의 일반적인 경향성을 파악하는 데 유용하며, 데이터 분석, 보고서 작성, 성능 평가 등 다양한 상황에서 활용될 수 있습니다.
‘AVG’ 사용 예
전체 평균 계산 : 모든 직원의 평균 급여를 계산하고 싶을 때
SELECT AVG(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에 있는 값들의 평균을 계산합니다.
그룹별 평균 계산 : 각 부서별 직원들의 평균 급여를 계산하고 싶을 때
SELECT department, AVG(salary) FROM employees GROUP BY department;
이 쿼리는 ‘employees’ 테이블에서 각 ‘department’ 별로 ‘salary’ 열의 평균을 계산하여, 각 부서의 직원들에 대한 평균 급여를 보여줍니다.
조건부 평균 계산 : 2023년에 이루어진 모든 판매 건에 대한 평균 판매액을 계산하고 싶을 때
SELECT AVG(sales_amount) FROM sales WHERE year = 2023;
이 쿼리는 ‘salse’ 테이블에서 ‘year’ 열이 2023인 모든 행의 ‘sales_amount’ 열 값들의 평균을 반환합니다.
‘AVG’ 함수의 특징
‘AVG’ 함수는 숫자 데이터에 대해서만 사용할 수 있습니다. 문자열이나 날짜 등 다른 타입의 데이터에는 사용할 수 없습니다.
‘NULL’ 값을 포함하는 열에 ‘AVG’ 함수를 사용할 때, ‘NULL’ 값은 계산에서 제외됩니다.
즉, ‘NULL’ 값은 평균 계산에 영향을 주지 않으며, 실제 값이 있는 데이터만을 기준으로 평균이 계산됩니다.
‘AVG’ 는 다른 집계 함수(‘SUM’, ‘COUNT’, ‘MIN’, ‘MAX’ 등)와 함께 사용될 수 있으며, 데이터의 통계적 분석이나 요약 정보 제공에 유용합니다.
사용 시 고려사항
‘AVG’ 함수를 사용할 때는 대상 열이 숫자 타입인지 확인해야 합니다.
데이터 세트의 크기가 클 때 ‘AVG’ 함수를 사용하면 쿼리 성능에 영향을 줄 수 있으므로, 필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화해야 합니다.
‘GROUP BY’ 절과 함께 ‘AVG’ 를 사용할 때는, 그룹화할 열을 명확하게 지정해야 하며, 그룹별로 평균값을 계산하고자 할 때 특히 유용합니다.
‘AVG’ 함수는 데이터 세트에서 평균값을 계산하여 중요한 인사이트를 제공하는 집계 함수로, 데이터 분석과 의사 결정 과정에서 핵심적인 역할을 합니다.
COUNT
‘COUNT’ 함수는 SQL에서 행(row)의 수를 세는 데 사용됩니다.
이 함수는 특정 조건을 만족하는 행의 수를 찾거나, 테이블의 전체 행 수를 계산할 때 매우 유용합니다. 데이터 분석, 보고서 작성, 데이터 집합의 크기를 파악하는 등의 상황에서 활용됩니다.
‘COUNT’ 는 다양한 형태로 사용될 수 있으며, 가장 일반적인 사용 방법은 ‘COUNT(*)’, COUNT(열 이름), 그리고 ‘COUNT(DISTINCT 열 이름)’ 입니다.
‘COUNT’ 사용 예
테이블의 전체 행 수 계산 : ‘employees’ 테이블의 전체 직원 수를 계산하고 싶을 때
SELECT COUNT(*) FROM employees;
이 쿼리는 ‘employees’ 테이블의 전체 행 수를 반환합니다.
특정 조건을 만족하는 행 수 계산 : 연봉이 $50,000 이상인 직원의 수를 찾고 싶을 때
SELECT COUNT(*) FROM employees WHERE salary >= 50000;
이 쿼리는 ‘salary’가 $50,000 이상인 행의 수를 반환합니다.
고유값의 수 계산 : ‘employees’ 테이블에서 고유한 부서의 수를 계산하고 싶을 때
SELECT COUNT(DISTINCT department) FROM employees;
이 쿼리는 중복을 제거한 ‘department’ 열의 고유값 수를 반환합니다.
‘COUNT’ 함수의 특징.
‘COUNT(*)’ 는 테이블의 전체 행 수를 세며, ‘NULL’ 값을 포함한 모든 행을 계산합니다.
‘COUNT(열 이름)’ 는 특정 열에서 ‘NULL’ 이 아닌 행의 수를 세는 데 사용됩니다.
‘COUNT(DISTINCT 열 이름)’ 는 특정 열의 고유값 수를 계산할 때 사용되며, 중복된 값은 하나로 취급합니다.
‘COUNT’ 함수는 집계 함수로 분류되며, ‘GROUP BY’ 절과 함께 사용하여 특정 조건에 따른 그룹별 행 수를 계산하는 데 유용합니다.
사용 시 고려사항
‘COUNT(*)’ 와 COUNT(열 이름) 사이에는 성능 차이가 있을 수 있으므로, 사용 상황에 따라 적절한 형태를 선택하는 것이 중요합니다.
대규모 데이터베이스에서 ‘COUNT’ 쿼리를 실행할 때는 쿼리 성능에 주의해야 하며, 필요한 경우 적절한 인덱스를 사용하거나 조건을 최적화하여 성능을 개선할 수 있습니다.
‘COUNT’ 함수는 데이터베이스 내 데이터의 양을 측정하고 분석하는 데 필수적인 도구로, 데이터의 크기나 특정 조건을 만족하는 데이터의 수를 파악하는 데 매우 유용합니다.
MIN
MIN 함수는 SQL에서 특정 열(column)의 최소값을 찾을 때 사용됩니다.
이 함수는 숫자, 문자열, 날짜 데이터 타입 등 다양한 종류의 데이터에 대해 작동하며, 테이블 전체 또는 특정 조건을 만족하는 데이터 집합 내에서 가장 작은 값을 찾는 데 유용합니다.
‘MIN’ 은 주로 데이터 분석, 보고서 작성, 데이터의 범위를 이해하고자 할 때 사용됩니다.
‘MIN’ 사용 예
숫자 데이터의 최소값 찾기 : 직원들의 최소 급여를 찾고 싶을 때
SELECT MIN(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에서 가장 낮은 급여를 반환합니다.
날짜 데이터의 최소값 찾기 : 가장 오래된 주문의 날짜를 찾고 싶을 때
SELECT MIN(order_date) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘order_date’ 열에서 가장 이른 날짜를 반환합니다.
문자열 데이터의 최소값 찾기 : 알파벳 순으로 가장 먼저 오는 제품 이름을 찾고 싶을 때
SELECT MIN(product_name) FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_name’ 열에서 알파벳 순으로 가장 앞서는 이름을 반환합니다. 문자열 데이터의 경우, ‘최소값’은 알파벳 순 또는 설정된 정렬 순서에 따라 결정됩니다.
‘MIN’ 함수의 특징
‘MIN’ 함수는 집계 함수의 하나로, 단일 열에서 가장 작은 값을 찾는 데 사용됩니다.
숫자, 문자열, 날짜 등 다양한 타입의 데이터에 대해 최소값을 찾을 수 있습니다.
‘GROUP BY’ 절과 함께 사용하면, 특정 기준(예: 부서별, 카테고리별)으로 그룹화된 데이터 내에서 각 그룹의 최소값을 찾는 데 사용할 수 있습니다.
사용 시 고려사항
‘MIN’ 함수를 사용할 때는 데이터 타입과 해당 필드의 데이터 구조를 이해하는 것이 중요합니다.
특히, 문자열 데이터에 대한 ‘MIN’ 의 사용은 예상치 못한 결과를 가져올 수 있으므로 주의가 필요합니다.
대규모 데이터셋에서 ‘MIN’ 함수를 사용할 때는 쿼리의 성능에 주의해야 합니다.
필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘MIN’ 함수는 데이터 세트에서 최소값을 식별할 때 필수적인 도구로, 데이터의 범위를 파악하고 특정 조건에 따른 최소값을 분석하는 데 유용합니다.
MAX
‘MAX’ 함수는 SQL에서 특정 열(column)의 최대값을 찾을 때 사용됩니다.
숫자, 문자열, 날짜 등 다양한 데이터 타입에 적용할 수 있으며, 테이블 전체 또는 특정 조건을 만족하는 데이터 집합 내에서 가장 큰 값을 찾는 데 유용합니다.
‘MAX’ 는 데이터의 상한을 파악하거나, 가장 최신 또는 가장 오래된 데이터를 식별하는 등의 상황에서 사용됩니다.
‘MAX’ 사용 예
숫자 데이터의 최대값 찾기 : 직원들의 최대 급여를 찾고 싶을 때
SELECT MAX(salary) FROM employees;
이 쿼리는 ‘employees’ 테이블의 ‘salary’ 열에서 가장 높은 급여를 반환합니다.
날짜 데이터의 최대값 찾기 : 가장 최근 주문의 날짜를 찾고 싶을 때
SELECT MAX(order_date) FROM orders;
이 쿼리는 ‘orders’ 테이블의 ‘order_date’ 열에서 가장 최근의 날짜를 반환합니다.
문자열 데이터의 최대값 찾기 : 알파벳 순으로 가장 마지막에 오는 제품 이름을 찾고 싶을 때
SELECT MAX(product_name) FROM products;
이 쿼리는 ‘products’ 테이블의 ‘product_name’ 열에서 알파벳 순으로 가장 뒤에 오는 이름을 반환합니다.
문자열 데이터의 경우, ‘최대값’은 알파벳 순 또는 설정된 정렬 순서에 따라 결정됩니다.
‘MAX’ 함수의 특징
‘MAX’ 함수는 집계 함수의 하나로, 단일 열에서 가장 큰 값을 찾는 데 사용됩니다.
숫자, 문자열, 날짜 등 다양한 타입의 데이터에 대해 최대값을 찾을 수 있습니다.
‘GROUP BY’ 절과 함꼐 사용하면, 특정 기준(예: 부서별, 카테고리별)으로 그룹화된 데이터 내에서 각 그룹의 최대값을 찾는 데 사용할 수 있습니다.
사용 시 고려사항
‘MAX’ 함수를 사용할 때는 데이터 타입과 해당 필드의 데이터 구조를 이해하는 것이 중요합니다. 특히, 문자열 데이터에 대한 ‘MAX’ 의 사용은 예상치 못한 결과를 가져올 수 있으므로 주의가 필요합니다.
대규모 데이터셋에서 ‘MAX’ 함수를 사용할 때는 쿼리의 성능에 주의해야 합니다. 필요한 경우 적절한 인덱스 사용과 데이터 필터링을 통해 성능을 최적화할 수 있습니다.
‘MAX’ 함수는 데이터 세트에서 최대값을 식별할 때 필수적인 도구로, 데이터의 범위를 파악하고 특정 조건에 따른 최대값을 분석하는 데 유용합니다.
-
-
💾 [CS] 소스코드와 명령어
소스코드와 명령어.
‘컴퓨터는 명령어를 처리하는 기계’
명령어는 컴퓨터를 실질적으로 작동시키는 매우 중요한 정보
모든 소스 코드(C, C++, Java, Python 과 같은 프로그래밍 언어로 만든 소스 코드)는 컴퓨터 내부에서 명령어로 변환됩니다.
고급 언어와 저급 언어
프로그램을 만들 때 사용하는 프로그래밍 언어, 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어
이렇게 ‘사람을 위한 언어’를 고급 언어(high-level programming language) 라고 합니다.
컴퓨터가 직접 이해하고 실행할 수 있는 언어
저급 언어(low-level programming language) 하고 합니다.
컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐입니다.
그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 합니다.
저급 언어에는 두 가지 종류가 있습니다.
기계어
0과 1의 명령어 비트로 이루어진 언어입니다.
다시 말해 0과 1로 이루어진 명령어 모음입니다.
어셈블리어
0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어
컴파일 언어와 인터프리터 언어
고급 언어는 저급 언어로 변환되는 방식으로는 크게 두 가지 방식이 있습니다.
컴파일 방식
컴파일 방식으로 작동하는 프로그래밍 언어를 컴파일 언어
인터프리트 방식
인터프리트 방식으로 작동하는 프로그래밍 언어를 인터프리터 언어
컴파일 언어
컴파일 언어
컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어입니다.
컴파일(Compile)
컴파일 언어로 작성된 소스 ㅋ코드는 전체가 저급 언어로 변환되는 과정을 거치는데 이 과정을 “컴파일”이라고 합니다.
컴파일러(Compiler)
컴파일을 수행해 주는 도구
개발자가 작성한 소스 코드 전체를 쭉 훑어보며 소스 코드에 문법적인 오류는 없는지, 실행 가능한 코드인지, 실행 가능한 코드인지, 실행하는 데 불필요한 코드는 없는지 등을 따지며 소스 코드를 처음부터 끝까지 저급 언어로 컴파일합니다.
이때 컴파일러가 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패합니다.
목적 코드(Object Code)
컴파일이 성공적으로 수행되면 개발자가 작성한 소스 코드는 컴퓨터가 이해할 수 있는 저급 언어로 변환됩니다.
이렇게 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드(Object code) 라고 합니다.
인터프리어 언어
인터프리터 언어
인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어입니다
대표적인 인터프리터 언어로 Python이 있습니다.
인터프리터
소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구
인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없습니다.
소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행됩니다.
일반적으로 인터프리터 언어는 컴파일 언어보다 느립니다.
컴파일을 통해 나온 결과물, 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스코드 마지막에 이를 때까지 한 줄 한 줄씩 저급언어로 실행해야 하기 때문입니다.
목적 파일 vs 실행 파일
목적 파일
목적 코드로 이루어진 파일입니다.
실행 파일
윈도우의 .exe 확장자를 가진 파일이 대표적인 실행 파일입니다.
목적 코드가 실행 파일이 되기 위해서는 링킹이라는 작업을 거쳐야 합니다.
링킹
여러 개의 오브젝트 파일이나 라이브러리를 하나의 실행 파일로 결합하는 과정을 의미합니다. 컴파일러가 소스 코드를 기계어로 번역한 후 링커(Linker)가 이러한 기계어 코드들을 모아 실행 가능한 프로그램을 만듭니다.
키워드로 정리하는 핵심 포인트
고급 언어는 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다.
저급 언어는 컴퓨터가 직접 이해하고 실행할 수 있는 언어입니다.
저급 언어는 0과 1로 이루어진 명령어로 구성된 기계어와 기계어를 사람이 읽기 편한 형태로 번역한 어셈블리어가 있습니다.
컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 언어입니다.
인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 저급 언어로 변환되어 실행되는 언어 입니다.
Q1. Swift는 일반적으로 고급 언어로 분류됩니다. Swift의 어떤 특징이 개발자에게 고급 언어의 장점을 제공한다고 생각하나요?
Swift는 고급 언어의 특징으로 높은 수준의 추상화, 강력한 타입 시스템, 메모리 안전성, 그리고 빠른 개발 시간을 제공합니다. Swift의 옵셔널 타입과 같은 기능은 안전한 코드 작성을 돕고, ARC는 메모리 관리를 단순화합니다.
Q2. 고급 언어와 저급 언어의 차이점은 무엇이라고 생각하나요?
고급 언어는 인간이 이해하기 쉬운 형태로 추상화된 언어로, 복잡한 작업을 간단하게 표현할 수 있게 해줍니다. Java와 같은 고급 언어는 메모리 관리, 객체 지향 프로그래밍, 에러 처리 등 복잡한 컴퓨팅 개념을 추상화하여 개발자가 더 쉽게 소프트웨어를 개발할 수 있도록 돕습니다.
저급 언어는 컴퓨터가 직접 이해할 수 있는 더 낮은 수준의 명령어로 구성됩니다. 이에 해당하는 언어는 어셈블리 언어나 기계어로, 이들은 하드웨어와 밀접한 작업을 수행하는 데 사용됩니다. 저급 언어를 사용하면 성능 최적화와 메모리 관리를 더 세밀하게 제어할 수 있지만, 개발과 디버깅 과정이 복잡해집니다.
Q3. Java는 고급 언어 중 하나로 간주됩니다. Java에서 저급 언어의 특성을 활용할 수 있는 방법에는 어떤 것이 있나요?
Java는 기본적으로 고급 언어의 특성을 많이 가지고 있지만, JNI(Java Native Interface)를 통해 저급 언어 코드와 상호 작용할 수 있습니다. JNI는 Java 애플리케이션 내에서 C나 C++과 같은 저급 언어로 작성된 코드를 호출하고 사용할 수 있는 방법을 제공합니다. 이를 통해 개발자는 특정 작업을 위해 시스템 호출이나 하드웨어 직접 제어와 같은 저급 언어의 성능과 효율성을 Java 애플리케이션에 통합할 수 있습니다. 또한, 고성능을 요구하는 애플리케이션의 특정 부분에서 성능을 최적화할 수 있습니다.
Q4. Java에서 고급 언어의 특성이 백엔드 개발에 어떤 장점을 제공하나요?
Java의 고급 언어 특성은 백엔드 개발에서 여러 가지 장점을 제공합니다. 첫째, 강력한 객체 지향 프로그래밍(OOP) 지원으로 코드의 재사용성, 확장성, 유지 보수성이 향상됩니다. 둘째, 자동 메모리 관리와 가비지 컬렉션으로 메모리 누수와 같은 문제를 방지하며 개발자가 메모리 관리에 덜 신경 쓰고 로직 개발에 더 집중할 수 있게 합니다. 셋째, 다양한 라이브러리와 프레임워크, 그리고 강력한 개발 도구와 커뮤니티 지원으로 개발 속도와 효율성이 증가합니다. 마지막으로, Java는 플랫폼 독립적인 특성을 가지고 있어, 다양한 운영 체제에서 실행될 수 있는 애플리케이션을 개발할 수 있습니다.
-
💉[SQL] SQL 문의 기본 구조, SQL
SQL
SQL은 “데이터베이스와 대화를 하기 위한 언어”
SQL(Structured Query Language)은 데이터베이스 관리를 위해 널리 사용되는 쿼리 언어.
Query(쿼리)
데이터베이스에 저장된 데이터에 접근하거나 조직하기 위한 명령어.
SQL 문의 기본 구조
SELECT # '데이터 조회'의 명령어로 필수 구문
FROM # '어디에서 데이터를 조회할까'의 명령어로 필수 구문
WHERE # 조건을 지정해주는 구문
조건을 지정하는 방법.
비교 연산자: <, >, =, <>(같지 않다)
다양한 구문: IN, BETWEEN, LIKE
여러가지 조건의 적용: AND, OR, NOT
참고 자료
테이블과 컬럼, SQL
WHERE란?
AND, OR, NOT
BETWEEN, IN, LIKE
-
-
☕️[Java] Object와 OCP
Object와 OCP.
만약 Object가 없고, 또 Object가 제공하는 toString()이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다.
여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻 입니다.
아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다.
BadObjectPrinter
public class BadObjectPrinter {
public static void print(Car car) { // Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { // Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); // dogInfo() 메서드 만듬
System.out.println(string);
}
}
구체적인 것에 의존
BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog를 사용하는 것을 BadObjectPrinter는 Car, Dog에 의존한다고 표현합니다.
다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있습니다.(물론 직접 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
추상적인 것에 의존
앞서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용합니다.
이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 Object에 클래스에 의존한다고 표현합니다.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
ObjectPrinter는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다.
Animal과 Dog, Cat의 관계를 떠올려봅시다.
Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog, Cat과 같이 하위 타입으로
내려갈 수록 개념은 더 구체적이게 됩니다.
ObjectPrinter와 Object를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
ObjectPrinter의 print() 메서드와 전체 구조를 분석해봅시다.
다형적 참조 : print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용합니다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다.
메서드 오버라이딩 : Object는 모든 클래스의 부모입니다. 따라서 Dog, Car와 같은 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있습니다.
따라서 print(Object obj) 메서드는 Dog, Car와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있습니다.
OCP 원칙
OCP 원칙을 떠올려 봅시다.
Open : 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요가 없습니다.
ObjectPrinter는 모든 타입의 부모인 Object를 사용하고, Object가 제공하는 toString() 메서드만 사용합니다.
따라서 ObjectPrinter를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
System.out.println()
지금까지 설명한 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식을 설명하기 위해 만든 것입니다.
System.out.println() 메서드도 Object 매개변수를 사용하고 내부에서 toString()을 호출합니다.
따라서 System.out.println()를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다.
지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 어버라이딩해서 사용할 수 있도록 설계되어 있습니다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다.
쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다.
동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다. 앞서 ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다.
어떤 경우에는 Car 인스턴스가 넘어오고, 어떤 경우에는 Dog 인스턴스가 넘어옵니다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다.
참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다.
예) ObjectPrinter는 Object에 의존합니다.
-
💉[SQL] BETWEEN, IN, LIKE
BETWEEN, IN, LIKE
BETWEEN
‘BETWEEN’ 연산자는 SQL에서 특정 범위 내의 값을 선택할 때 사용됩니다.
이 연산자는 시작 값과 끝 값 사이에 있는 값을 찾는 데 사용되며, 포함 관계는 양 끝값을 포함합니다.
‘BETWEEN’ 은 숫자, 텍스트, 날짜 등 다양한 데이터 타입에 적용할 수 있어, 매우 유연하게 사용됩니다.
‘BETWEEN’ 사용 예
숫자 범위 : 나이가 20세에서 30세 사이인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age BETWEEN 20 AND 30;
날짜 범위 : 2023년 1월 1일부터 2023넌 12월 31일까지 생성된 모든 주문을 찾고 싶을 때
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';
텍스트 범위 : 알파벳 순으로 ‘apple’과 ‘banana’사이에 오는 모든 항목을 선택할 때
SELECT * FROM products WHERE name BETWEEN 'apple' AND 'banana';
‘BETWEEN’의 특징.
‘BETWEEN’ 연산자는 시작 값과 끝 값 모두를 포함하는 “닫힌 범위(closed range)”를 정의합니다.
범위 검색을 할 때 매우 효과적입니다. 예를 들어, 특정 기간 동안의 데이터 또는 특정 범위의 값을 갖는 데이터를 찾는 경우에 적합합니다.
‘BETWEEN’ 대신 ’>=’ 와 ’<=’ 를 사용해 동일한 조건을 표현할 수도 있지만, ‘BETWEEN’ 을 사용하는 것이 더 직관적이고 간결할 수 있습니다.
주의사항
텍스트 범위를 사용할 때는 데이터베이스가 사용하는 문자열 정렬 규칙(collation)에 주의해야 합니다. 이는 대소문자 구분, 알파벳 순서 등에 영향을 미칠 수 있습니다.
날짜 범위를 다룰 때는 날짜 포맷과 시간대 설정이 예상한 결과에 영향을 줄 수 있으니, 데이터베이스의 날짜 포맷 설정을 확인해야 합니다.
IN
‘IN’ 연산자는 SQL에서 한 번의 쿼리로 여러 값을 조회할 때 사용됩니다.
특히, 하나의 열(column)이 여러 개의 가능한 값 중 하나를 갖고 있는지 확인할 때 유용합니다.
‘IN’ 은 주어진 값 리스트 중 어느 하나라도 일치하는 행을 찾을 때 사용되며, 리스트 내의 값과 정확히 일치하는 행만을 결과로 반환합니다.
이는 복수의 ‘OR’ 조건을 사용하는 것과 동일한 결과를 나타내지만, 훨씬 간결하고 읽기 쉬운 쿼리를 작성할 수 있게 해줍니다.
‘IN’ 사용 예
다수의 명시적 값에 대한 검색 : 이름이 ‘Alice’, ‘Bob’, 또는 ‘Charlie’인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE name IN ('Alice', 'Bob', 'Charlie');
서브쿼리와 함께 사용 : 특정 조건을 만족하는 다른 테이블의 값에 해당하는 행을 찾을 때
SELECT * FROM product WHERE category_id IN (SELECT id FROM categories WHERE type = 'Eletronics')
리스트에 포함된 값들로 필터링 : 특정 지역 코드를 가진 모든 전화번호를 찾고 싶을 때
SELECT * FROM phone_number WHERE area_code IN ('202', '303', '404');
‘IN’ 연산자의 특징
‘IN’ 은 주어진 리스트 안에 있는 값과 일치하는 모든 행을 찾아내는 데 사용됩니다.
복수의 ‘OR’ 조건을 간단하게 표현할 수 있어, 쿼리의 가독성을 높여줍니다.
리스트 내의 각 항목은 정확한 일치(match)를 찾는 데 사용되므로, 부분 일치나 패턴 일치를 위해서는 다른 연산자(예: ‘LIKE’)를 사용해야 합니다.
‘IN’ 은 서브쿼리와 함께 사용될 때 매우 강력하며, 다른 테이블의 결과에 기반한 쿼리를 작성하는 데 유용합니다.
사용 시 고려사항
‘IN’ 리스트에 많은 수의 값이 포함될 경우, 성능이 저하될 수 있습니다. 가능한 한, ‘JOIN’ 이나 다른 방법으로 쿼리를 최적화하는 것을 고려해야 합니다.
‘IN’ 으로 서브 쿼리를 사용할 때는 서브쿼리가 많은 양의 데이터를 반환하지 않도록 주의해야 합니다. 서브쿼리의 결과가 크면 큰 만큼, 전체 쿼리의 성능에 영향을 줄 수 있습니다.
‘IN’ 연산자는 SQL에서 특정한 값들의 집합에 대해 검색할 때 매우 유용하며, 쿼리의 복잡성을 줄이고 읽기 쉽게 만들어 줍니다.
LIKE
‘LIKE’ 연산자는 SQL에서 패턴 매칭을 통해 데이터를 검색할 때 사용됩니다.
이는 주로 텍스트 데이터를 다룰 때 유용하며, 특정 패턴이나 일부 문자열이 포함된 행을 찾고자 할 때 활용됩니다.
‘LIKE’ 연산자는 와일드카드 문자와 함께 사용되며, 더 유연한 검색 조건을 제공합니다.
주로 사용되는 와일드카드에는 ‘%’(어떤 문자열이든지 대체 가능)와 ‘_‘(단인 문자 대체)가 있습니다.
‘LIKE’ 사용 예
특정 문자열로 시작하는 데이터 검색 : 이름이 ‘J’로 시작하는 사람을 찾고 싶을 때
SELECT * FROM people WHERE name LIKE 'J%';
이 경우, ‘J’로 시작하는 모든 이름을 찾습니다. ‘%’는 ‘J’ 이후에 어떤 문자열이 와도 괜찮다는 것을 의미합니다.
특정 문자열을 포함하는 데이터 검색 : 이메일 주소에 ‘gmail.com’을 포함하는 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE email LIKE '%gmail.com';
여기서 ‘%’는 ‘gmail.com’ 앞에 어떤 문자열이 오든지 상관 없다는 것을 의미합니다.
특정 패턴에 맞는 데이터 검색 : 세 자리 코드 중 두 번째 자리가 ‘A’인 모든 코드를 찾고 싶을 때
SELECT * FROM codes WHERE code LIKE '_A%';
이 쿼리에서 ‘_‘는 정확히 하나의 문자를 대체하고, ‘%’는 그 뒤에 어떤 문자열이 오든지 상관 없다는 것을 의미합니다.
‘LIKE’ 연산자의 특징
‘LIKE’ 는 대소문자를 구분하는 데이터베이스에서는 대소문자가 정확히 일치하는 경우에만 결과를 반환합니다. 대소문자 구분 없이 검색하려면, 데이터베이스 또는 컬럼의 설정을 확인하거나, 쿼리에 특정 함수를 사용해야 할 수 있습니다.
패턴 매칭을 통해 유연한 검색이 가능하지만, 와일드카드를 많이 사용할수록 쿼리의 성능은 떨어질 수 있습니다. 특히, 문자열 시작 부분에 ‘%’를 사용하는 경우 인덱스 활용이 어려워 성능 저하의 원인이 될 수 있습니다.
‘LIKE’ 연산자는 문자열 필드 내에서 특정 패턴이나 부분 문자열을 기반으로 데이터를 검색할 때 매우 유용합니다.
패턴 매칭 기능을 통해 복잡한 조건의 문자열 검색을 수행할 수 있으며, 데이터 분석이나 데이터 정제 과정에서 중요한 역할을 합니다.
-
💉[SQL] AND, OR, NOT
AND, OR, NOT
AND
‘AND’ 연산자는 SQL에서 여러 조건을 동시에 만족해야 할 때 사용됩니다.
즉, ‘AND’ 를 사용하는 쿼리는 모든 조건인 참(TRUE)일 때만 결과를 반환합니다.
이는 데이터베이스에서 더 세밀한 필터링을 수행하고자 할 때 유용하며, 특히 복잡한 데이터 집합에서 기준에 부합하는 정확한 데이터를 찾고자 할 때 중요한 역할을 합니다.
‘AND’ 사용 예
여러 기준에 따른 데이터 검색 : 나이가 30세 이상이면서 ‘New York’에 거주하는 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age >= 30 AND city = 'New York' ;
이 쿼리는 ‘age’ 열이 30 이상이면서 동시에 ‘city’ 열이 ‘New York’인 모든 행을 반환합니다.
날짜 범위와 특정 조건을 동시에 만족하는 데이터 검색 : 2023년 1월 1일부터 2023년 3월 31일 사이에 등록되고, 상태가 ‘활성화’인 모든 계정을 찾고 싶을 때
SELECT * FROM account WHERE registration_date BETWEEN '2023-01-01' AND '2023-03-31' AND status = 'Active';
이 쿼리는 ‘registration_date’ 가 지정된 날짜 범위 내에 있으며, ‘status’ 가 ‘Active’ 인 행을 반환합니다.
‘AND’ 연산자의 특징
‘AND’ 연산자를 사용할 때는 각 조건이 서로 어떤 관계에 있는지 고려해야 합니다. 예를 들어, 상호 배타적인 조건을 ‘AND’ 로 연결하면 결과가 항상 비어 있을 것입니다.
성능에 영향을 미칠 수 있는 큰 데이터 세트에서는 인덱스와 조건의 효율적인 사용이 중요합니다. 가능한 한, 성능에 영향을 덜 미치는 조건을 먼저 적용하는 것이 좋습니다.
‘AND’ 연산자는 복수의 조건을 조합하여 데이터를 필터링하고자 할 때 필수적인 도구입니다.
이를 통해 더 정확하고 의미 있는 데이터 집합을 얻을 수 있으며, SQL 쿼리 작성 시 다양한 상황에 맞춰 유연하게 적용할 수 있습니다.
OR
‘OR’ 연산자는 SQL에서 주어진 조건 중 하나 이상이 참(TRUE)일 때 결과를 반환하고자 할 때 사용됩니다.
‘OR’ 을 사용하면 여러 조건 중 하나라도 만족하는 데이터를 선택할 수 있어, 데이터베이스 쿼리의 유연성을 크게 향상시킬 수 있습니다.
‘OR’ 은 다양한 시나리오에서 유용하게 사용되며, 특히 여러 다른 가능성을 모두 포함해야 할 때 중요한 역할을 합니다.
‘OR’ 사용 예
여러 다른 값 중 하나를 만족하는 데이터 검색 : ‘Manager’ 또는 ‘Sales’ 부서에 속한 모든 직원을 찾고 싶을 때
SELECT * FROM employees WHERE department = 'Manager' OR department = 'Sales';
이 쿼리는 ‘department’ 열이 ‘Manager’이거나 ‘Sales’인 모든 행을 반환합니다.
여러 조건 중 하나 이상을 만족하는 데이터 검색 : 나이가 18세 미만이거나 65세 이상인 모든 사람을 찾고 싶을 때
SELECT * FROM people WHERE age < 18 OR age >= 65;
이 쿼리는 ‘age’ 열이 18세 미만이거나 65세 이상인 모든 행을 반환합니다.
‘OR’ 연산자의 특징
‘OR’ 연산자를 사용할 때는 주어진 조건 중 하나라도 참이면 결과 집합에 해당 행이 포함됩니다. 모든 조건이 거짓(FALSE)인 경우에만 결과에서 제외됩니다.
여러 개의 다른 가능성을 허용하는 데 유용하며, 특히 사용자의 입력이나 선택에 따라 다양한 결과를 보여줘야 할 때 자주 사용됩니다.
‘AND’ 연산자와 함께 사용될 수 있으나, 이 경우 우선 순위에 주의해야 하며, 괄호를 사용하여 연산자 간의 우선 순위를 명확하게 구분해야 합니다.
사용 시 고려사항
‘OR’ 연산자를 사용할 때는 쿼리의 성능에 주의해야 합니다. 특히 대규모 데이터 셋에서는 ‘OR’ 조건이 많을 수록 쿼리 성능이 저하될 수 있습니다.
가능한 경우, ‘OR’ 을 사용하는 대신 다른 접근 방식을 고려해보는 것도 좋습니다. 예를 들어, ‘IN’ 연산자를 사용하면 ‘OR’ 과 유사한 결과를 더 효율적으로 얻을 수 있을 때가 많습니다.
복잡한 쿼리에서는 ‘OR’ 과 ‘AND’ 를 혼합하여 사용할 때 괄호를 적절히 사용하여 명확한 논리 구조를 유지하는 것이 중요합니다.
‘OR’ 연산자는 다양한 조건을 유연하게 처리하고자 할 때 매우 유용하며, SQL 쿼리를 작성하는 과정에서 필요한 결과를 얻기 위해 다양한 시나리오를 고려할 수 있게 해줍니다.
NOT
‘NOT’ 연산자는 SQL에서 조건의 논리를 부정할 때 사용됩니다.
즉, ‘NOT’ 은 특정 조건이 거짓(FALSE)일 때 참(TRUE)인 결과를 반환하도록 합니다.
이를 통해 특정 조건을 만족하지 않는 데이터를 검색하고자 할 때 매우 유용하게 활용할 수 있습니다.
‘NOT’ 연산자는 ‘WHERE’ 절 내에서 다른 연산자(예: ‘IN’, ‘BETWEEN’, ‘LIKE’, ‘EXISTS’)와 함께 사용되어, 해당 조건의 반대되는 결과를 얻고자 할 때 사용됩니다.
‘NOT’ 사용 예
특정 조건을 만족하지 않는 데이터 검색 : ‘Sales’ 부서에 속하지 않는 모든 직원을 찾고 싶을 때
SELECT * FROM employees WHERE NOT department = 'Sales';
이는 ‘department’ 가 ‘Sales’ 가 아닌 모든 행을 반환합니다.
특정 범위에 속하지 않는 데이터 검색 : 20세에서 30세 사이가 아닌 사람을 찾고 싶을 때
SELECT * FROM people WHERE NOT age BETWEEN 20 AND 30;
이는 나이가 20세 이상 30세 이하가 아닌 모든 사람을 찾습니다.
지정된 목록에 포함되지 않는 데이터 검색 : ‘Manager’와 ‘Sales’ 부서에 속하지 않은 모든 직원을 찾고 싶을 때
SELECT * FROM employee WHERE department NOT IN ('Manager', 'Sales');
이 쿼리는 ‘department’ 가 ‘Manager’ 또는 ‘Sales’가 아닌 모든 행을 반환합니다.
‘NOT’ 연산자의 특징.
‘NOT’ 연산자는 조건의 논리를 부정하여, 조건이 거짓일 때 참을 반환합니다.
‘NOT’ 은 ‘WHERE’ 절에서 다양한 연산자와 함께 사용될 수 있으며, 특정 조건을 제외한 데이터를 선택하고자 할 때 유용합니다.
복잡한 조건에서는 ‘NOT’ 을 사용하여 예외적인 경우를 쉽게 필터링할 수 있습니다.
사용 시 고려사항.
‘NOT’ 연산자를 사용할 때는 쿼리의 성능에 주의해야 합니다. 특히, ‘NOT’ 이 포함된 조건은 인덱스 활용이 어려워 성능 저하를 일으킬 수 있습니다.
명확하지 않은 논리를 피하기 위해, 가능한 한 ‘NOT’ 의 사용을 최소화하고, 대신 명확한 조건을 사용하여 원하는 결과를 얻는 것이 좋습니다.
‘NOT’ 연산자는 SQL 쿼리에서 특정 조건을 만족하지 않는 데이터를 필터링하고자 할 때 유용하게 사용됩니다.
하지만, 쿼리의 명확성과 성능을 고려하여 신중하게 사용해야 합니다.
-
💉[SQL] WHERE란?
WHERE ?
SQL에서 ‘WHERE’ 절은 데이터베이스에서 특정 조건을 만족하는 행(row)을 검색할 때 사용됩니다.
기본적으로 ‘SELECT’, ‘UPDATE’, ‘DELETE’ 문에서 데이터를 필터링하기 위해 사용되며, 이를 통해 반환되거나 영향을 받는 데이터의 범위를 좁힐 수 있습니다.
WHERE 절의 기본 구조
SELECT column1, column2, ...
FROM table_name
WHERE conditionl
‘SELECT’ 문에서는 특정 조건을 만족하는 행을 선택해 반환합니다.
‘UPDATE’ 문에서는 특정 조건을 만족하는 행에 대해서만 업데이트를 수행합니다.
‘DELETE’ 문에서는 특정 조건을 만족하는 행을 삭제합니다.
조건의 사용
‘WHERE’ 절에서 사용할 수 있는 조건에는 다음과 같은 것들이 있습니다.
비교 연산자(’=’, ‘!=’, ‘<’, ‘>’, ‘<=’, ‘>=’)
논리 연산자(‘AND’, ‘OR’, ‘NOT’)
범위 검색(‘BETWEEN’)
목록에서 선택(‘IN’)
패턴 매칭(‘LIKE’)
NULL 값 검사(‘IS NULL’)
예시
예를 들어, 이름이 ‘Jhon’인 사람의 정보를 찾고 싶다면 다음과 같이 쿼리를 작성할 수 있습니다.
SELECT * FROM users WHERE name = 'Jhon';
또는 나이가 18세 이상인 모든 사용자를 찾고 싶다면 다음과 같이 작성할 수 있습니다.
SELECT * FROM users WHERE age >= 18;
‘WHERE’ 절을 사용함으로써, 큰 데이터베이스 내에서도 필요한 데이터를 효율적으로 찾아낼 수 있습니다.
-
☕️[Java] toString()
toString()
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
코드로 확인해봅시다.
package lang.object.tostring;
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@a09ee92
java.lang.Object@a09ee92
Object.toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다
println()과 toString()
그런데 toString()의 결과를 출력한 코드와 object를 println()에 직접 출력한 코드의 결과가 완전히 같습니다.
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
System.out.println() 메서드는 사실 내부에서 toString()을 호출합니다.
Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력합니다.
public void println(Object x) {
String s = String.valueOf(x);
// ...
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
따라서 println()을 사용할 때, toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
toString() 오버라이딩.
Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 toString()을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package lang.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package lang.object.tostring;
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
package lang.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package lang.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("3. Object 다용성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다용성 활용
객체 정보 출력: lang.object.tostring.Car@452b3a41
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
Car 인스턴스는 toString()을 재정의 하지 않습니다.
따라서 Object가 제공하는 기본 toString() 메서드를 사용합니다.
Dog 인스턴스는 toString()을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
Object obj의 인수로 car(Car)가 전달됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. 재정의된 메서드가 없습니다.
Object.toString()을 실행합니다.
ObjectPrinter.print(dog) // main에서 호출
void print(Object obj = dog(Dog)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
Object obj의 인수로 dog(Dog) 가 전달 됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. Dog에 재정의된 메서드가 있습니다.
Dog.toString()을 실행합니다.
참고 - 객체의 참조값 직접 출력
toString()은 기본으로 객체의 참조값을 출력합니다.
그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue)
실행 결과
refValue = 72ea2f77
-
-
-
-
-
-
💉[SQL] 테이블과 컬럼, SQL
SQL?
SQL은 “데이터베이스와 대화를 하기 위한 언어” 입니다.
옆의 사람에세 필요한 것을 요청시 “A를 주시겠어요?”와 하는 것과 같이 “DB에게도 A를 주시겠어요?” 라고 이야기할 때 사용하는 언어라고 할 수 있습니다.
Query
SQL 이란 언어를 이용하여 데이터베이스에 요청을 하는 질의를 ‘Query’라고 합니다.
테이블과 컬럼?
데이터베이스 : 쉽게 말해 “데이터가 저장되어있는 큰 폴더” 입니다.
체계적으로 조직된 데이터의 집합으로, 데이터의 저장, 검색, 수정, 삭제 등을 효율적으로 처리할 수 있게 해주는 데이터 구조와 관리 시스템을 말합니다.
데이터베이스 관리 시스템(DBMS)은 이러한 데이터베이스를 만들고 관리하는 소프트웨어입니다.
데이터베이스의 주요 구성 요소.
데이터(Data) : 정보의 원시 형태로, 문자, 숫자, 이미지 등 다양한 형태가 있습니다. 데이터베이스에 저장된 데이터는 조직화되어 있어 효율적인 접근과 관리가 가능합니다.
테이블(Table) : 데이터를 구조화하여 저장하는 기본 단위입니다. 테이블은 행(Row)과 열(Column)로 구성되어 있으며, 각 행은 고유한 데이터 레코드를, 열은 특정 데이터 필드를 나타냅니다.
스키마(Schema) : 데이터베이스의 구조를 정의하는 메타데이터의 집합입니다. 테이블 구조, 데이터 타입, 관계 등 데이터베이스의 뼈대를 이룹니다.
쿼리(Query) : 데이터베이스에 저장된 데이터에 접근하거나 조직하기 위한 명령어입니다. SQL(Structured Query Language)은 데이터베이스 관리를 위해 널리 사용되는 쿼리 언어입니다.
데이터베이스의 중요성.
중복성 감소 : 데이터베이스는 데이터 중복을 최소화하여 저장 공간의 효율성을 높이고 데이터 일관성을 유지합니다.
데이터 무결성 : 데이터베이스는 데이터의 정확성과 일관성을 유지하기 위한 규칙(제약 조건)을 적용합니다. 이를 통해 데이터의 신뢰성을 보장합니다.
보안 : 데이터베이스는 사용자의 권한을 관리하여 특정 데이터에 대한 접근을 제어할 수 있습니다. 이는 데이터의 보안을 강화합니다.
백업 및 복구 : 데이터베이스는 데이터의 백업 및 복구 기능을 제공하여, 시스템 장애나 데이터 손실 시 데이터를 복원할 수 있습니다.
데이터베이스의 종류.
관계형 데이터베이스(RDBMS) : 테이블 간의 관계를 기반으로 하는 데이터베이스입니다. Oracle, MySQL, PostgreSQL 등이 있습니다.
비관계형 데이터베이스(NoSQL) : 스키마가 없거나 유연한 데이터 모델을 사용하여 대규모 분산 데이터를 관리하는 데이터베이스입니다. MongoDB, Cassandra, Redis 등이 있습니다.
-
💉[SQL] 데이터베이스 모델링
건물을 짓기 위한 설계도: 데이터베이스 모델링(Database Modeling)
테이블의 구조를 미리 설계하는 개념으로 건출 설계도를 그리는 과정과 비슷합니다.
프로젝트를 진행하기 위해서는 대표적으로 “폭포수 모델(waterfall model)” 을 사용하며, 데이터베이스 모델링은 폭포수 모델의 업무 분석과 시스템 설계 단계에 해당합니다.
이 단계를 거치면 가장 중요한 데이터베이스 개체인 “테이블 구조” 가 결정되는 것 입니다.
프로젝트 진행 단계.
“프로젝트(project)”
현실 세계에서 일어나는 업무를 컴퓨터 시스템으로 옮겨놓는 과정.
대규모 소프트웨어(software) 를 작성하기 위한 전체 과정.
프로그램과 소프트웨어의 구분
프로그래밍 언어(C, 자바, 파이썬 등)를 통해서 만들어진 결과물을 소프트웨어(software)라고 부릅니다.
소프트웨어와 프로그램(program)은 거의 비슷한 용어로 소프트웨어는 좀 더 큰 단위, 프로그램은 좀 더 작은 단위로 부르기도 하지만 대부분의 상황에서 구분 없이 사용하고 있습니다.
“폭포수 모델(waterfall model)”
소프트웨어 개발 절차 중 하나
각 단계가 폭포가 떨어지듯 진행되기 때문에 붙여진 이름
폭포수 모델의 단계
프로젝트 계획
업무 분석
시스템 설계
프로그램 구현
테스트
유지보수
각 단계의 의미를 예를 들어 설명해보겠습니다.
지금 우리가 슈퍼마켓을 운영하고 있다고 가정해봅시다.
이 슈퍼마켓의 물건을 온라인으로도 판매하기 위해 인터넷 쇼핑몰을 구축하려고 합니다.
프로젝트 계획 : 슈퍼마켓의 물건들을 온라인으로 판매하기 위한 계획 단계입니다.
업무 분석 : 슈퍼마켓에서 업무가 어떻게 돌아가는지 파악하는 것입니다. 예로 물건은 어디서 들어오는지, 물건을 어떻게 계산하는지, 재고는 어떻게 관리하는지 등의 업무에 대해서 정리하는 단계입니다.
시스템 설계 : 앞에서 정리한 업무 분석을 컴퓨터에 적용시키기 위해서 알맞은 형태로 다듬는 과정입니다.
프로그램 구현 : 앞에서 완성한 시스템 설계의 결과를 실제 프로그래밍 언어로 코딩하는 단계입니다. 우리가 계획한 내용을 온라인으로 제공하기 위해서는 JavaScript, PHP, JSP 등의 프로그래밍 언어를 사용해야 합니다.
테스트 : 코딩된 프로그램에 오류가 없는지 확인하는 과정입니다.
유지보수 : 실제 온라인 쇼핑몰을 운영하면서 문제점을 보안하고 기능을 추가하는 과정입니다.
폭포수 모델의 장.단점
장점 : 각 단계가 구분되어 프로젝트의 진행 단계가 명확하다는 장점.
단점 : 폭포에서 내려가기는 쉬워도 다시 거슬러 올라가기는 힘든 것처럼 문제가 발생할 경우 다시 앞 단계로 돌아가기가 어렵다는 단점
데이터베이스 모델링.
현실 세계의 슈퍼마켓을 인터넷 쇼핑몰로 만드는 프로젝트를 바탕으로 데이터베이스 모델링 부분을 살펴보겠습니다.
“데이터베이스 모델링(Database modeling)”
우리가 살고 있는 세상에서 사용되는 사물이나 작업을 DBMS의 데이터베이스 개체롤 옮기가 위한 과정
쉽게 이야기하면 현실에서 쓰이는 것을 테이블로 변경하기 위한 작업
슈퍼마켓(현실 세계)의 고객, 물건, 직원 등을 데이터베이스에 각각의 테이블 이라는 개체로 변환합니다.
예를 들어 어떤 사람의 신분을 증명하기 위한 신분증에 이름, 주민등록번호, 주소 등의 정보가 있는 것과 비슷한 개념입니다.
인터넷 쇼핑몰에서 판매할 제품들도 마찬가지입니다. 제품의 이름, 가격, 제조일자, 제조회사, 재고량 등을 데이터베이스에 저장하는 것 입니다.
데이터베이스 모델링에는 정답이 없습니다.
다만, 좋은 모델링과 나쁜 모델링은 분명히 존재합니다. 이는 다양한 학습과 실무 경험에서 우러나옵니다.
전체 데이터베이스 구성도
앞에서 살펴본 데이터베이스 모델링의 결과로 다음과 같은 구성이 완료되었다고 가정하겠습니다.
데이터(data) : 하나하나의 단편적인 정보를 말합니다. 이 그림에서는 tess, 아이유, 바나나와 같은 개별적인 정보를 말합니다.
테이블(table) : 회원이나 제품의 데이터를 입력하기 위해 표 형태로 표현한 것을 말합니다. 지금은 인터넷 쇼핑몰을 구현하기 위해서 회원 정보를 보관할 회원 테이블과 제품 정보를 보관할 제품 테이블, 2개의 테이블을 만들었습니다.
데이터베이스(database) : 테이블이 저장되는 저장소를 말합니다. 데이터를 저장하는 곳이라는 의미로 그림에서는 원통 모양으로 표현했습니다. 그림에 3개의 데이터베이스를 표현했는데요, 각 데이터베이스는 이름이 서로 달라야합니다.
DBMS(Database Management System) : 데이터베이스 관리 시스템 또는 소프트웨어를 말합니다. MySQL 과 같은것이 바로 DBMS입니다. 그림에서 MySQL이 3개의 데이터베이스를 관리하고 있습니다.
열(column) : 테이블의 세로를 말합니다. 각 테이블은 여러 개의 열(컬럼, 필드)로 구성됩니다. 회원 테이블은 3개의 열로, 제품 테이블은 5개의 열로 구성되어 있습니다.
열 이름 : 각 열을 구분하기 위한 이름입니다. 열 이름은 각 테이블 내에서는 서로 달라야 합니다. 회원 테이블의 아이디, 회원 이름, 주소 등이 열 이름입니다.
데이터 형식 : 열에 저장될 데이터의 형식을 말합니다. 회원 테이블의 회원 이름은 열은 ‘1234’와 같은 숫자가 아닌 ‘나훈아’와 같은 문자 형식이어야 합니다. 그리고 제품 테이블의 가격 열은 숫자(정수) 형식이어야 합니다. 데이터 형식은 테이블을 생성할 때 열 이름과 함께 지정해줍니다.
행(row) : 실질적인 진짜 데이터를 말합니다. 예로, ‘tess/나훈아/경기 부천시 중동’이 하나의 행(로우, 레코드)으로 행 데이터라고도 부릅니다. 회원 테이블에서 회원이 몇 명인지는 행 데이터가 몇 개인지로 알 수 있습니다. 즉, 행의 개수가 데이터의 개수입니다. 이 예에서는 4건의 행 데이터가 있으므로 4명의 회원이 가입되어 있는 것입니다.
기본 키(Primary Key, PK) : 기본 키(또는 주키) 열은 각 행을 구분하는 유일한 열을 말합니다. 더 쉽게는 네이버의 회원 아이디, 학번, 주민등록번호 같은 것이라고 생각하면 됩니다. 그래서 기본 키는 중복되어서는 안 되며, 비어 있어서도 안 됩니다.
네이버 아이디, 학번, 주민등록번호 등이 다른 사람과 중복되지 않습니다. 또 네이버 회원인데 네이버 아이디가 없거나, 한국 사람인데 주민등록번호가 없는 것은 불가능합니다.
테이블에는 열이 여러 개 있지만 기본 키는 1개만 지정해야 하며, 일반적으로 1개의 열에 지정합니다.
SQL(Structure Query Language) : DBMS에서 작업을 하고 싶다면 DBMS가 알아듣는 언어(말)로 해야 합니다. 그것이 SQL(구조화된 질의 언어)입니다. 즉, SQL은 사람과 DBMS가 소통하기 위한 언어입니다.
4가지 핵심 키워드, 핵심 포인트
프로젝트란 현실 세계에서 컴퓨터 시스템으로 옮겨놓는 일련의 과정입니다.
폭포수 모델은 소프트웨어 개발 단계 중 하나로, 이름 그대로 폭포가 떨어지듯 개발 단계가 진행됩니다.
데이터베이스 모델링이란 현실 세계에서 사용되는 작업이나 사물들을 DBMS의 테이블(표 형태로 표현한 데이터베이스 개체)로 옮기기 위한 과정입니다.
-
-
-
💾 [CS] 0과 1로 문자를 표현하는 방법
0과 1로 문자를 표현하는 방법.
문자 집합과 인코딩.
반드시 알아야 할 세 가지 용어
문자 집합
인코딩
디코딩
컴퓨터가 인식하교 표현할 수 있는 문자의 모음을 “문자 집합(character set)” 이라고 합니다.
문자를 0과 1로 변환하는 과정을 “문자 인코딩(character encoding)” 이라고 합니다.
0과 1로 이루어진 문자 코드를 사람이 이해할 수 있는 문자로 변환하는 과정을 “문자 디코딩(character decoding)” 이라고 합니다.
아스키 코드.
아스키(ASCII: American Standard Code for Information Interchang)
초창기 문자 집합 중 하나
영어 알파벳과 아라비아 숫자, 그리고 일부 특수 문자를 포함합니다.
아스키 문자
각각 7비트로 표현되는데, 7비트로 표현할 수 있는 정보의 가짓수는 2⁷개로, 총 128개의 문자를 표현할 수 있습니다.
아스키 코드
표를 보면 알 수 있듯 아스키 문자들은 0부터 127까지 총 128개의 숫자 중 하나의 고유한 수에 일대일로 대응됩니다. 아스키 문자에 대응된 고유한 수를 “아스키 코드”라고 합니다.
아스키 코드로 인코딩
아스키 코드를 이진수로 표현함으로써 아스키 문자를 0과 1로 표현할 수 있습니다.
아스키 문자는 이렇게 아스키 코드로 인코딩됩니다.
아스키 코드의 장,단점.
장점
매우 간단하게 인코딩됩니다.
단점
한글을 표현할 수 없습니다.
한글뿐만 아니라 아스키 문자 집합 외의 문자, 특수문자도 표현할 수 없습니다.
그 이유는 근본적으로 아스키 문자 집합에 속한 문자들은 7비트로 표현하기에 128개보다 많은 문자를 표현하지 못하기 때문입니다.
확장 아스키(Extend ASCII).
더 다양한 문자 표현을 위해 아스키 코드에 1비트를 추가한 8비트의 아스키 코드.
그럼에도 표현 가능한 문자 수는 256개여서 턱없이 부족했습니다.
EUC-KR.
한국을 포함한 영어권 외의 나라들은 자신들의 언어를 0과 1로 표현할 수 있는 고유한 문자 집합과 인코딩 방식이 필요하다고 생각했습니다.
이러한 이유로 등장한 한글 인코딩 방식
EUC-KR은 KS X 1001, KS X 1003이라는 문자 집합을 기반으로하는 대표적인 완성형 인코딩 방식입니다.
즉, 초성 중성, 종성이 모두 결합된 한글 단어에 2바이크 크기의 코드를 부여합니다.
EUC-KR로 인코딩된 한글 한 글자를 표현하려면 16비트(한글 한 글자에 2바이트 코드 부여)가 필요합니다.
16비트는 네 자리 십육진수로 표현할 수 있습니다.
즉, EUC-KR로 인코딩된 한글은 네 자리 십육진수로 나타낼 수 있습니다.
한글 인코딩의 두 가지 방식.
완성형 인코딩.
조합형 인코딩.
“완성형 인코딩”
초성, 중성, 종성의 조합으로 이루어진 하나의 글자에 고유한 코드를 부여하는 인코딩 방식입니다.
조합형 인코딩
초성을 위한 비트열, 중성을 위한 비트열, 종성을 위한 비트열을 할당하여 그것들의 조합으로 하나의 글자 코드를 완성하는 인코딩 방식입니다.
다시 말해 초성, 중성, 종성에 해당하는 코드를 합하여 하나의 글자 코드를 만드는 인코딩 방식입니다.
EUC-KR의 문제점.
아스키 코드보다 표현할 수 있는 문자가 많아졌지만(총 2,350여개), 이는 모든 한글 조합을 표현할 수 있을 정도로 많은 양은 아닙니다.
그래서 문자 집합에 정의되지 않은 ‘쀍’, ‘쀓’, ‘믜’같은 글자는 EUC-KR로 표현할 수 없습니다.
“모든 한글을 표현할 수 없다는 사실은 때때로 크고 작은 문제를 유발합니다.”
EUC-KR 인코딩을 사용하는 웹사이트의 한글이 깨지는 현상.
EUC-KR 방식으로는 표현할 수 없는 이름으로 인해 은행, 학교 등에서 피해를 받는 사람이 생김.
이러한 문제를 조금이나마 해결하기 위해 등장한 것이 MS사의 “CP929(Code Page 949)” 입니다.
CP949는 EUC-KR의 확장된 버전
EUC-KR로는 표현할 수 없는 더욱 다양한 문자를 표현 할 수 있습니다.
다만, 이마저도 한글 전체를 표현하기에 넉넉한 양은 아닙니다.
유니코드와 UTF-8.
모든 나라 언어의 문자 집합과 인코딩 방식이 통일되어 있다면, 다시 말해 모든 언어를 아우르는 문자 집합과 통일된 표준 인코딩 방식이 있다면 언어별로 인코딩하는 수고로움을 덜 수 있을 겁니다.
그래서 등장한 것이 “유니코드(Unicode)” 문자 집합입니다.
유니코드.
EUC-KR보다 훨씬 다양한 한글을 포함하며 대부분 나라의 문자, 특수문자, 화살표나 이모티콘까지도 코드로 표현할 수 있는 통일된 문자집합힙니다.
현대 문자를 표현할 때 가장 많이 사용되는 표준 문자 집합이며, 문자 인코딩 세계에서 매우 중요한 역할을 맡고 있습니다.
UTF-8, 16, 32
유니코드는 글자에 부여된 값 자체를 인코딩된 값으로 삼지 않고 이 값을 다양한 방법으로 인코딩합니다.
이런 인코딩 방법에는 크게 UTF-8, 16, 32 등이 있습니다.
요컨데 UTF-8, 16, 32는 유니코드 문자에 부여된 값을 인코딩하는 방식입니다.
UTF-8
통상 1바이트부터 4바이트까지의 인코딩 결과를 만들어 냅니다.
UTF-8로 인코딩한 값의 결과는 1바니크가 될 수도 2바이트, 3바이트, 4바이트가 될 수도 있습니다.
UTF-8로 인코딩한 결과가 몇 바이트가 될지는 유니코드 문자에 부여된 값의 범위에 따라 결정됩니다.
4가지 키워드로 정리하는 핵심 포인트
문자 집합은 컴퓨터가 인식할 수 있는 문자의 모음으로, 문자 집합에 속한 문자를 인코딩하여 0과 1로 표현할 수 있습니다.
아스키 문자 집합에 0부터 127까지의 수가 할당되어 아스키 코드로 인코딩됩니다.
EUC-KR은 한글을 2바이트 크기로 인코딩할 수 있는 완성형 인코딩 방식입니다.
유니코드는 여러 나라의 문자들을 광범위하게 표현할 수 있는 통일된 문자 집합이며, UTF-8, 16, 32는 유니코드 문자의 인코딩 방식입니다.
Q1. iOS 개발에서 문자열을 다루는 것은 매우 흔한 작업입니다. 모든 문자는 컴퓨터 내부에서 0과 1의 이진 코드로 표현됩니다. 예를 들어, 유니코드 인코딩 방식 중 하나인 UTF-8을 사용하여 문자를 이진 코드로 변환할 수 있습니다. ‘안녕하세요’라는 문자열을 UTF-8 인코딩을 사용하여 이진 코드로 어떻게 변환할지 설명해 주세요. 또한, 이 과정에서 iOS 개발에 사용되는 Swift 언어에서 이러한 변환을 수행하는 코드 예시를 작성해 보세요.
UTF-8 인코딩 변환 과정 설명
‘안녕하세요’라는 문자열은 한글 문자로 구성되어 있으며, UTF-8 인코딩에서 한글은 보통 3바이트(24비트)로 인코딩됩니다. UTF-8은 가변 길이 인코딩 방식으로, 각 문자를 1바이트에서 4바이트까지 다양한 길이의 바이트로 인코딩합니다. 예를 들어, ASCII 코드의 경우 1바이트만 사용하지만, 한글과 같은 문자는 더 많은 바이트를 사용합니다.
예시로 ‘안녕하세요’ 중 ‘안’이라는 문자의 유니코드 코드 포인트는 U+548C입니다. 이를 UTF-8로 인코딩하면 다음과 같은 이진수로 표현될 수 있습니다: 1110xxxx 10xxxxxx 10xxxxxx. 실제 이진 코드로 변환하면 특정 이진값을 갖게 됩니다. (‘안’의 경우 실제 이진 변환 결과는 여기서 직접 계산하지 않았으나, 각 문자를 해당 방식으로 변환할 수 있습니다.)
안녕하세요’를 UTF-8로 인코딩하면 다음과 같은 이진 코드로 표현됩니다:
11101100 10010101 10001000 11101011 10000101 10010101 11101101 10010101 10011000 11101100 10000100 10111000 11101100 10011010 10010100
이진 코드는 각 바이트를 8비트 이진수로 표현한 것입니다.
UTF-8 인코딩에서 한글 문자는 대체로 3바이트로 인코딩되므로, 위의 이진 코드는 ‘안녕하세요’의 각 글자를 UTF-8 인코딩으로 변환한 결과를 보여줍니다.
각 부분이 한글 문자 하나를 나타내며, 각 문자는 3개의 바이트(24비트)로 이루어져 있습니다
Swift에서의 구현 예시
Swift에서 문자열을 UTF-8 이진 코드로 변환하는 것은 간단합니다. Swift의 String 타입은 utf8 프로퍼티를 통해 UTF-8 인코딩을 쉽게 접근할 수 있게 해줍니다.
let message = "안녕하세요"
var binaryString = ""
for codeUnit in message.utf8 {
binaryString += String(codeUnit, radix: 2) + " "
}
print(binaryString)
이 코드는 각 문자를 UTF-8 인코딩으로 변환한 후, 각 바이트를 이진수로 변환하여 출력합니다.
출력 결과는 각 UTF-8 인코딩된 바이트를 이진수 형태로 나타낸 것으로, 각 바이트 사이에는 공백이 있습니다.
Q2. Java에서는 문자와 문자열을 다루는 일이 자주 발생합니다. 특히 백엔드 시스템을 개발할 때, 다양한 인코딩 방식을 이해하고 이를 적절히 처리할 수 있는 능력이 중요합니다. UTF-8 인코딩 방식은 국제적으로 널리 사용되며, 다양한 언어와 특수 문자를 지원하는 강력한 인코딩 방식입니다. Java에서 문자열 ‘Java 백엔드 개발자’를 UTF-8 인코딩을 사용하여 이진 코드로 변환하는 과정을 설명해 주세요. 또한, 이 과정을 구현하는 Java 코드를 작성해 보세요.
주어진 질문에 대한 답변은 크게 두 부분으로 나눌 수 있습니다: 첫 번째는 UTF-8 인코딩 방식에 대한 이해와 설명이며, 두 번째는 ‘Java 백엔드 개발자’ 문자열을 UTF-8로 인코딩하여 이진 코드로 변환하는 Java 코드의 구현입니다.
1. UTF-8 인코딩 방식에 대한 이해
UTF-8은 유니코드 문자 집합을 인코딩하는 가장 널리 사용되는 방식 중 하나로, 1바이트에서 4바이트까지 다양한 길이의 바이트를 사용하여 전 세계의 거의 모든 문자를 표현할 수 있습니다. UTF-8은 영문 알파벳과 숫자 같은 기본적인 문자들을 1바이트로 표현하고, 그 외의 문자들은 2바이트 이상을 사용합니다. 예를 들어, 한글은 3바이트를 사용하여 표현됩니다. 이러한 특성 때문에, UTF-8은 다국어 처리가 필요한 웹 및 백엔드 시스템 개발에 널리 사용됩니다.
2. Java 코드 구현
‘Java 백엔드 개발자’ 문자열을 UTF-8로 인코딩하여 이진 코드로 변환하는 과정은 다음 Java 코드를 통해 구현할 수 있습니다:
public class Main {
public static void main(String[] args) {
String text = "Java 백엔드 개발자";
byte[] bytes = text.getBytes(java.nio.charset.StandardCharsets.UTF_8);
StringBuilder binaryString = new StringBuilder();
for (byte b : bytes) {
// 각 바이트를 이진수로 변환하고, 8자리 이진수 형태를 유지하기 위해 앞에 0을 채움
String binary = String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
binaryString.append(binary).append(" ");
}
System.out.println(binaryString.toString().trim());
}
}
이 코드는 다음과 같은 과정을 거칩니다:
문자열 “Java 백엔드 개발자”를 UTF-8 인코딩을 사용하여 바이트 배열로 변환합니다.
변환된 바이트 배열을 순회하면서, 각 바이트를 8비트 이진수로 변환합니다. 이 때, & 0xFF 연산을 사용하여 부호 없는 정수로 처리하고, String.format을 사용하여 이진수를 8자리로 맞춥니다.
변환된 이진수 문자열을 콘솔에 출력합니다.
이 구현을 통해 후보자는 UTF-8 인코딩 방식의 이해, Java에서의 문자열 처리, 그리고 바이트 및 이진수 처리에 대한 자신의 지식과 기술을 면접관에게 보여줄 수 있습니다. 이는 Java 백엔드 개발자로서 갖추어야 할 중요한 기술 중 하나입니다.
-
-
☕️[Java] 좋은 객체 지향 프로그래밍이란?
좋은 객체 지향 프로그래밍이란?
객체 지향 특징
추상화
캡슐화
상속
다형성
객체 지향 프로그래밍?
객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 “객체” 들의 모임으로 파악하고자 하는 것입니다.
각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있습니다.(협력)
객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.
유연하고, 변경에 용이?
레고 블럭 조립하듯이
키보드, 마우스 갈아 끼우듯이
컴퓨터 부품 갈아 끼우듯이
컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
다형성(Polymorphism)
다형성의 실세계 비유
실세계와 객체 지향을 1:1로 매칭 X
그래도 실세계의 비유로 이해하기에는 좋음
역할과 구현으로 세상을 구분
운전자 - 자동차
공연무대
로미오와 줄리렛 공연
예시
운전자 - 자동차
공연 무대
키보드, 마우스, 세상의 표준 인터페이스들
정렬 알고리즘
할인 정책 로직
역할과 구현을 분리
역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해집니다.
장점
클라이언트는 대상의 역할(인터페이스)만 알면 됩니다.
클라이언트는 구현 대상의 내부 구조를 몰라도 됩니다.
클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않습니다.
클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않습니다.
역할과 구현을 분리 2
자바 언어
자바 언어의 다형성을 활용합니다.
역할 = 인터페이스.
구현 = 인터페이스를 구현한 클래스, 구현 객체.
객체를 설계할 때 역할과 구현을 명확히 분리합니다.
객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다.
객체의 협력이라는 관계부터 생각
혼자 있는 객체는 없습니다.
클라이언트: 요청, 서버 응답
수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가집니다.
자바 언어의 다형성
오버라이딩을 떠올려봅시다.
오버라이딩은 자바 기본 문법입니다.
오버라이딩 된 메서드가 실행합니다.
다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있습니다.
물론 클래스 상속 관계도 다형성, 오버라이딩 적용 가능합니다.
다형성의 본질
인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다.
다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야 합니다.
클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있습니다.
역할과 구현을 분리 3
정리
실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있습니다.
유연하고, 변경이 용이합니다.
확장 가능한 설계입니다.
클라이언트에 영향을 주지 않는 변경이 가능합니다.
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
한계
역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생합니다.
자동차를 비행기로 변경해야 한다면?
대본 자체가 변경된다면?
USB 인터페이스가 변경된다면?
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
정리
다형성이 가장 중요합니다!
디자인 패턴 대부분은 다형성을 활용하는 것입니다.
스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것입니다.
다형성을 잘 활용하면 마치 레고 블럭 조립하듯이! 공연 무대의 배우를 선택하듯이! 구현을 편리하게 변경할 수 있습니다.
-
-
-
-
☕️[Java] 인터페이스
인터페이스
“자바는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공합니다.”
순수 추상 클래스
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
“인터페이스는 class 가 아니라 interface 키워드를 사용하면 됩니다.”
인터페이스
public interface InterfaceAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스 - public abstract 키워드 생략 가능
public interface InterfaceAnimal {
void sound();
void move();
}
순수 추상 클래스는 다음과 같은 특징을 갖습니다.
인스턴스를 생성할 수 없습니다.
상속시 모든 메서드를 오버라이딩 해야 합니다.
주로 다형성을 위해 사용됩니다.
인터페이스는 앞서 설명한 순수 추상 클래스와 같습니다. 여기에 약간의 편의 기능이 추가 됩니다.
인터페이스 메서드는 모두 public, abstract입니다.
메서드에 public abstract를 생략할 수 있습니다.(참고로 생략이 권장됩니다.)
인터페이스는 다중 구현(다중 상속)을 지원합니다.
인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final int MY_PI = 3.14;
}
인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주됩니다.
final은 변수의 값을 한 번 설정하면 수정할 수 없다는 뜻입니다.
자바에서 static final을 사용해 정적이면서 고칠 수 없는 변수를 상수라 하고, 관례상 상수는 대문자에 언더스코어(_)로 구분합니다.
해당 키워드는 다음과 같이 생략할 수 있습니다.(생략이 권장됩니다.)
public interface InterfaceAnimal {
int MY_PI = 3.14;
}
예제 5
클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속)관계는 UML에서 점선을 사용합니다.
package poly.ex5;
public class InterfaceMain {
public static void main(String[] args) {
// 인터페이스 생성 불가
// InterfaceAnimal interfaceAnimal = new InterfaceAnimal();
Cat cat = new Cat();
Dog dog = new Dog();
Caw caw = new Caw();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(caw);
}
// 변하지 않는 부분
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
앞서 설명한 순수 추상 클래스 예제와 거의 유사합니다.
순수 추상 클래스가 인터페이스가 되었을 뿐입니다.
클래스, 추상 클래스, 인터페이스는 모두 똑같습니다.
클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같습니다.
모두 자바에서는 .class로 로 다루어집니다.
인터페이스를 작성할 때도 .java에 인터페이스를 정의합니다.
인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됩니다.
상속 vs 구현
부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현합니다.
이렇게 서로 다르게 표현하는 이유를 알아봅시다.
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적입니다.
하지만 인터페이스는 모든 메서드가 추상 메서드입니다.
따라서 물려 받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 합니다.
따라서 구현한다고 표현합니다.
인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 합니다.
따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현합니다.
상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같습니다. 일반 상속 구조와 동일하게 작동합니다.
인터페이스를 사용해야 하는 이유.
모든 메서드가 추상 메서드인 경우 순수 추상 클래스로 만들어도 되고, 인터페이스를 만들어도 됩니다.
그런데 왜 인터페이스를 사용해야 할까요? 단순히 편리하다는 이유를 넘어서 다음과 같은 이유가 있습니다.
제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것입니다.
USB 인터페이스를 생각해봅시다. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 합니다. 그렇지 않으면 작동하지 않습니다.
인터페이스의 규약(제약)은 반드시 구현해야 하는 것입니다.
그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있습니다.
이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 됩니다.
인터페이스는 모든 메서드가 추상 메서드입니다. 따라서 이런 문제를 원천 차단할 수 있습니다.
다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있습니다.
반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능합니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
참고
자바 8에 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있습니다.
하지만 이것은 예외적으로 아주 특별한 경우에만 사용해야 합니다.
자바 9에서 등장한 인터페이스의 private 메서드도 마찬가지입니다.
지금 학습 단계에서는 이 부분들을 고려하지 않는 것이 좋습니다.
이 부분은 추후에 따로 학습하고 정리할 것 입니다.
-
💾 [CS] 컴퓨터 메모리를 16진수로 표시하는 이유
컴퓨터 메모리를 16진수로 표시하는 이유.
이진수와의 호환성 : 컴퓨터는 모든 데이터를 이진수, 즉 0과 1로 처리합니다. 이진수는 매우 기본적이지만, 긴 이진수를 읽고 이해하기는 어렵습니다. 16진수는 이진수를 좀 더 읽기 쉽게 만들어 줍니다. 4비트 이진수 한 덩어리가 16진수 한 자리와 정확히 대응되기 때문에, 이진수를 16진수로 변환하는 것은 자연스럽고 효율적입니다. 예를 들어, 이진수 1111은 16진수 F로 표현됩니다.
효율적인 표현 : 16진수를 사용하면 매우 큰 수나 메모리 주소를 훨씬 짧고, 관리하기 쉬운 형태로 표현할 수 있습니다. 예를 들어, 8비트 이진수인 10011011은 16진수로는 단 두 자리 9B로 표현할 수 있습니다. 이는 프로그래머와 기술자가 메모리 주소나 데이터 값을 빠르게 인식하고 작업하기 용이합니다.
표준화와 호환성 : 16진수는 컴퓨터 과학과 전자공학에서 널리 표준화되어 사용됩니다. 소프트웨어 개발, 디버깅, 하드웨어 설계 등 다양한 분야에서 16진수 사용은 정보를 일관되게 표현하고 전달하는 데 도움을 줍니다. 이는 서로 다른 시스템과 기술 간의 호환성을 증진시키는 역할을 합니다.
디버깅과 분석 용이 : 개발자와 엔지니어가 시스템의 문제를 진단하거나 메모리의 내용을 분석할 때, 16진수 표현은 이진 데이터를 빠르게 읽고 해석할 수 있게 해줍니다. 이는 소프트웨어와 하드웨어의 오류를 찾고 해결하는 과정을 간소화합니다.
이렇게 16진수는 이진수의 복잡성을 줄이면서도 정보를 효과적으로 표현하고 처리할 수 있는 효율적인 방법을 제공합니다.
컴퓨터 공학에서 이러한 방식을 사용함으로써, 우리는 컴퓨터 시스템과 소프트웨어를 보다 쉽게 이해하고, 효율적으로 작업할 수 있게 됩니다.
-
💾 [CS] 0과 1로 숫자를 표현하는 방법
0과 1로 숫자를 표현하는 방법.
정보 단위.
컴퓨터는 0 또는 1밖에 이해하지 못합니다.
0과 1을 나타내는 나타내는 가장 작은 정보 단위를 “비트(bit)” 라고 합니다.
비트는 0 또는 1, 두 가지 정보를 표현할 수 있습니다.
“n비트는 2ⁿ가지 정보를 표현할 수 있습니다.”
바이트(byte) : 여덟 개의 비트를 묶은 단위로, 비트보다 한 단계 큰 단위.
1바이트는 8비트와 같습니다.
2⁸(256)개의 정보를 표현할 수 있습니다.
킬로바이트(kB: kilobyte) : 1바이트 1,000개를 묶은 단위입니다.
메가바이트(MB: megabyte) : 1킬로바이트 1,000개를 묶은 단위입니다.
기가바이트(GB: gigabyte) : 1메가바이트 1,000개를 묶은 단위입니다.
테라바이트(TB: terabyte) : 1기가바이트 1,000개를 묶은 단위입니다.
더 큰 단위도 있습니다.
워드(word) : CPU가 한 번에 처리할 수 있는 데이터 크기를 의미합니다.
만약 CPU가 한 번에 16비트를 처리할 수 있다면 1워드는 16비트가 되고, 한 번에 32비트를 처리할 수 있다면 1워드는 32비트가 되는 것입니다.
워드의 절반 크기를 하프 워드(half word), 1배 크기를 풀 워드(full word), 2배 크기를 더블 워드(double word) 라고 부릅니다.
컴퓨터의 워트 크기는 대부분 32비트 또는 64비트 입니다. 가령 인텔의 x86 CPU는 32비트 워드, x64 CPU는 64비트 워드 CPU입니다.
이진법
0과 1만드로 모든 숫자를 표현하는 방법을 “이진법(binary)” 라고 합니다.
우리가 일상적으로 사용하는 방법은 십진법(decimal) 라고 합니다.
이진법으로 표현한 수를 “이진수”
십진법으로 표현한 수를 “십진수”
숫자만으로 어떤 수가 어떤 진법으로 표현된 수인지 알 수 없습니다.
이런 혼동을 예방하기 위해 이진수 끝에 아래첨자 (2)를 붙이거나 이진수 앞에 0b를 붙입니다.
전자는 주로 이진수를 수학적으로 표기할 때, 후자는 주로 코드 상에서 이진수를 표기할 때 사용합니다.
이진수의 음수 표현
음수를 표현하는 방법 중 가장 널리 사용되는 방법은 2의 보수(two;s complement) 를 구해 이 값을 음수로 간주하는 방법입니다.
2의 보수의 사전적 의미: ‘어떤 수를 그보다 큰 2ⁿ에서 뺀 값’을 의미합니다.
예를 들어 11₍₂₎의 2의 보수는 11₍₂₎보다 큰 2ⁿ, 즉 100₍₂₎에서 11₍₂₎을 뺀 01₍₂₎이 되는 것 입니다.
“굳이 이렇게 사전적 의미로 어렵게 이해할 필요는 없습니다. 2의 보수를 매우 쉽게 표현하자면 다음과 같습니다.”
‘모든 0과 1을 뒤집고, 거기에 1을 더한 값’으로 이해하면 됩니다.
예를 들어 11₍₂₎의 모든 0과 1을 뒤집으면 00₍₂₎이고, 거기에 1을 더한 값은 01₍₂₎입니다.
즉, 11₍₂₎의 2의 보수(음수 표현)는 01₍₂₎이 됩니다.
“실제로 이진수만 봐서는 이게 음수인지 양수인지 구분하기 어렵습니다. 그래서 컴퓨터 내부에서 어떤 수를 다룰 때는 이 수가 양수인지 음수인지를 구분하기 위해 ‘플래그(flag)’를 사용합니다.”
플래그는 쉽게 말해 부가 정보입니다.
십육진법(hexadecimal)
수가 15를 넘어가는 시점에 자리 올림을 하는 숫자 표현 방식입니다.
그리고 십진수 10, 11, 12, 13, 14, 15를 십육진법 체계에서는 각각 A, B, C, D, E, F로 표기합니다.
십육진수도 이진수와 마찬가지로 숫자 뒤에 아래첨자 ₍₁₆₎를 븉아고너 숫자 앞에 0x룰 븉여 구분합니다.
전자는 주로 수학적으로 표기할 때 사용되는 방식
후자는 주로 코드상에서 십육진수를 표기할 때 사용되는 방식
십육진법을 사용하는 주된 이유 중 하나는 이진수를 십육진수로, 십육진수를 이진수로 변환하기 쉽기 때문입니다.
십육진수를 이진수로 변환하기.
십육진수는 한 글자당 열여섯 종류(0~9, A~F)의 숫자를 표현할 수 있습니다.
십육진수를 이루는 숫자 하나를 이진수로 표현할 때는 4비트가 필요합니다.(2⁴ = 16)
십육진수를 이준수로 변환하는 간편한 방법 중 하나는 십육진수 한 글자를 4비트의 이진수로 간주하는 것 입니다.
즉, 십육진수를 이루고 있는 각 글자를 따로따로(4개의 숫자로 구성된) 이진수로 변환하고, 그것을 이어 붙이면 십육진수가 이진수로 변환됩니다.
이진수를 십육진수로 변환하기
이진수를 십육진수로 변환할 때는 이진수 숫자를 네 개씩 끊고, 끊어 준 네 개의 숫자를 하나의 십육진수로 변환한 뒤 그대로 이어 붙이면 됩니다.
키워드로 정리하는 핵심 포인트
비트는 0과 1로 표현할 수 있는 가장 작은 정보 단위입니다.
바이트, 킬로바이트, 메가바이트, 기가바이트, 테라바이트는 비트보다 더 큰 정보 단위입니다.
이진법은 1을 넘어가는 시점에 자리 올림을 하여 0과 1만으로 수를 표현하는 방법입니다.
이진법에서 음수는 2의 보수로 표현할 수 있습니다.
십육진법은 15를 넘어가는 시점에 자리 올림하여 수를 표현하는 방법입니다.
Q1.현대의 컴퓨터와 디지털 기기들은 데이터를 처리하고 저장할 때 기본적으로 0과 1, 즉 이진수를 사용합니다. iOS 개발 과정에서도 이러한 이진수의 원리를 이해하는 것이 중요한데요, 여러분은 이러한 이진수 시스템이 왜 필요하고, 어떻게 우리가 개발하는 앱과 관련이 있는지 설명해주실 수 있나요? 특히, 이진수의 개념이 iOS 앱 개발에서 어떤 실질적인 적용 사례를 가지는지 구체적인 예를 들어 주세요.
이진수 시스템은 컴퓨터와 디지털 기기들이 데이터를 처리하고 저장하는 기본적인 방법입니다. 이 시스템은 0과 1, 두 가지 상태만을 사용하여 정보를 표현하는 방법으로, 컴퓨터 하드웨어는 이러한 이진 상태들을 전기적 신호의 켜짐과 꺼짐으로 해석합니다. 이는 컴퓨터 기술에서 가장 기본이 되는 원리로, 모든 프로그래밍 언어와 운영 체제, 애플리케이션 개발에 깊이 관련되어 있습니다.
iOS 앱 개발에 있어 이진수의 이해는 몇 가지 중요한 측면에서 의미를 가집니다:
데이터 저장과 처리: 앱 내에서 사용자 데이터, 설정, 상태 정보 등을 저장하고 처리할 때, 이진 형식이 기본적으로 사용됩니다. 예를 들어, 사용자가 앱 내에서 사진을 찍거나 파일을 다운로드할 때, 이러한 데이터는 이진 형태로 디바이스에 저장됩니다.
통신: 앱이 서버와 데이터를 주고받을 때, 이진 데이터 형식이 널리 사용됩니다. 예를 들어, REST API를 통해 JSON 형식으로 데이터를 교환하더라도, 실제 네트워크를 통한 전송 과정에서는 이진 데이터로 변환되어 처리됩니다.
성능 최적화: 이진수를 직접 다루는 지식은 앱의 성능 최적화에 큰 도움이 될 수 있습니다. 예를 들어, 이미지나 동영상 처리, 암호화, 데이터 압축 등 고성능을 요구하는 작업에서는 낮은 수준의 이진 처리가 필요할 수 있습니다.
하드웨어 접근과 제어: iOS 앱 개발에서 때로는 하드웨어의 낮은 수준의 기능에 접근하거나 제어해야 할 필요가 있습니다. 이 경우, 이진수 처리 방식을 이해하는 것이 필수적입니다. 예를 들어, Bluetooth 통신이나 기타 특수한 하드웨어 기능을 사용하는 앱을 개발할 때 이진 데이터의 처리가 필요합니다.
이진수 시스템의 이해는 따라서, 기본적인 데이터의 표현부터 앱의 성능 최적화, 하드웨어 제어에 이르기까지 iOS 앱 개발의 여러 단계에 걸쳐 중요한 역할을 합니다. 이러한 지식은 개발자로서 문제 해결 능력을 향상시키고, 더 효율적이고 강력한 앱을 만드는 데 기여합니다.
Q2. 우리가 컴퓨터 과학에서 배우는 가장 기본적인 개념 중 하나는 모든 디지털 데이터가 궁극적으로 0과 1, 즉 이진수로 표현된다는 것입니다. 이러한 이진수 체계를 이해하는 것이 왜 Java 백엔드 개발에 있어 중요한지에 대해 설명해 주세요. 또한, 이 개념이 실제 백엔드 시스템 개발과 운영에 어떻게 적용될 수 있는지 구체적인 예를 들어 설명해주실 수 있나요?
이진수 체계의 이해는 Java 백엔드 개발에 있어 여러 가지 이유로 중요합니다:
데이터 표현 및 처리의 기본: 컴퓨터는 모든 정보를 이진수로 처리하고 저장합니다. Java 백엔드 개발자로서 데이터를 저장, 검색, 변환하는 다양한 작업을 수행할 때 이진 데이터의 이해는 필수적입니다. 예를 들어, 파일 시스템에서 데이터를 읽고 쓰거나, 네트워크 통신을 통해 데이터를 송수신할 때 이진 데이터 형식에 대한 지식이 필요합니다.
성능 최적화: 이진수에 대한 이해는 데이터 압축, 암호화, 데이터 전송 최적화와 같은 고급 개발 작업에서 성능을 향상시키는 데 도움이 됩니다. 예를 들어, 대용량 데이터를 효율적으로 처리하기 위해 비트 연산을 사용할 수 있으며, 이는 이진수의 원리를 이해할 때 가능해집니다.
암호화 및 보안: 현대의 암호화 알고리즘은 대부분 이진수 기반의 복잡한 수학적 연산을 사용합니다. 백엔드 시스템에서 사용자 데이터의 보안을 유지하기 위해 데이터를 암호화하고 해시 함수를 적용할 때, 이진수 원리의 이해는 필수적입니다.
하드웨어 및 시스템 인터페이스: 백엔드 시스템은 때로 특정 하드웨어나 시스템과 직접적으로 상호작용해야 할 수 있습니다. 이러한 상호작용은 종종 낮은 수준의 데이터 표현에 대한 깊은 이해를 요구하며, 이는 이진수 체계의 지식이 있을 때 효율적으로 수행될 수 있습니다.
이진수 체계의 이해는 Java 백엔드 개발자가 효율적이고 안전한 시스템을 설계하고 구현하는 데 필수적인 기초를 제공합니다. 데이터의 기본적인 표현 방식을 이해함으로써 개발자는 보다 깊은 수준에서 시스템을 이해하고, 성능과 보안 문제를 더 잘 해결할 수 있게 됩니다.
-
-
-
☕️[Java] 다형성 활용3
다형성 활용 3
이번에는 배열과 for문을 사용해서 중복을 제거해보겠습니다.
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Animal[] animalArr = {dog, cat, caw};
// 변하지 않는 부분
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
배열은 같은 타입의 데이터를 나열할 수 있습니다.
Dog, Cat, Caw는 모두 Animal의 자식이므로 Animal 타입입니다.
Animal 타입의 배열을 만들고 다형적 참조를 사용하면 됩니다.
// 둘은 같은 코드입니다.
Animal[] animalArr = new Anima[]{dog, cat, caw};
Animal[] animalArr = {dog, cat, caw};
다형적 참조 덕분에 Dog, Cat, Caw의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했습니다.
이제 배열을 for문을 사용해서 반복하면 됩니다.
// 변하지 않는 부분
for (Animal animal: animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
animal.sound()를 호출하지만 배열에는 Dog, Cat, Caw의 인스턴스가 들어있습니다.
메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출됩니다.
조금 더 개선
package poly.ex2;
public class AnimalPolyMain3 {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Cat()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
// 변하지 않는 부분
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
Animal[] animalArr를 통해서 배열을 사용합니다.
soundAnimal(Animal animal)
하나의 동물을 받아서 로직을 처리합니다.
새로운 동물이 추가되어도 soundAnimal(...) 메서드는 코드 변경 없이 유지할 수 있습니다.
이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Caw 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조하기 때문입니다.
따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 병경 없이 유지할 수 있습니다.
여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있습니다.
main()은 코드가 변하는 부분입니다.
새로운 동물을 생성하고 필요한 메서드를 호출합니다.
soundAnimal(...)는 코드가 변하지 않는 부분입니다.
“새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드입니다.”
이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋습니다.
남은 문제
지금까지 설명한 코드에는 사실 2가지 문제가 있습니다.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 클래스입니다.
이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까요?
Animal animal = new Animal();
개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상합니다.
이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없습니다.
하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없습니다.
누군가 실수로 new Animal()을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것 입니다.
이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않습니다.
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
예를들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해봅시다.
우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 “꿀꿀”이라는 소리가 나도록 하는 것입니다.
그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있습니다.
이렇게 되면 부모의 기능을 상속받습니다.
따라서 코드상 아무런 문제가 발생하지 않습니다.
물론 프로그램을 실행하면 기대와 다르게 “꿀꿀”이 아니라 부모 클래스에 있는 Animal.sound()가 호출될 것입니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있습니다.
-
-
☕️[Java] 다형성 활용1
다형성 활용1
지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보겠습니다.
아주 단순하고 전통적인 동물 소리 문제로 접근해보겠습니다.
개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해봅시다.
먼저 다형성을 사용하지 않고 코드를 작성해봅시다.
package poly.ex1;
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex1;
public class Cat {
public void sound() {
System.out.println("야옹");
}
}
package poly.ex1;
public class Caw {
public void sound() {
System.out.println("음메");
}
}
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램입니다.
만약 여기서 새로운 동물이 추가되면 어떻게 될까요?
만약 기존 코드에 소가 없다고 가정해봅시다.
소가 추가된다고 가정하면 Caw 클래스를 만들고 다음 코드도 추가해야 합니다.
// Caw를 생성하는 코드
Caw caw = new Caw();
// Caw를 사용하는 코드
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
Caw를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, Caw를 사용해서 출력하는 부분은 계속 중복이 증가합니다.
중복 코드
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 됩니다.
그런데 Dog, Cat, Caw는 서로 완전히 다른 클래스입니다.
중복 제거 시도
메서드로 중복 제거 시도
메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw, Dog, Cat 중에 하나로 정해야 합니다.
private static void soundCaw(Caw caw) {
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
따라서 이 메서드는 Caw 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없습니다.
Dog, Cat, Caw의 타입(클래스)이 서로 다르기 때문에 soundCaw 메서드를 함께 사용하는 것은 불가능합니다.
배열과 for문을 통한 중복 제거 시도
Caw[] cawArr = { cat, dog, caw }; // 컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound()
}
System.out.println("동물 소리 테스트 종료");
배열과 for문 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Caw 중에 하나로 지정해야 합니다.
같은 Caw들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Caw을 하나의 배열에 담는 것은 불가능합니다.
결과적으로 지금 상황에서는 해결방법이 없습니다.
새로운 동물이 추가될 때 마다 더 많은 중복 코드를 작성해야 합니다.
지금까지 설명한 모든 중복 제거 시도가 Dog, Cat, Caw의 타입이 서로 다르기 때문에 불가능합니다.
“문제의 핵심은 바로 타입이 다르다는 점” 입니다.
반대로 이야기하면 Dog, Cat, Caw가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것입니다.
다형성의 핵심은 다형적 참조와 메서드 오버라이딩입니다.
이 둘을 활용하면 Dog, Cat, Caw 가 모두 같은 타입을 사용하고, 각자 자신의 메서드로 호출할 수 있습니다.
-
💾 [CS] 컴퓨터 구조의 큰 그림
컴퓨터 구조의 큰 그림
우리가 알아야 할 컴퓨터 구조 지식은 크게 두 가지 입니다.
컴퓨터가 이해하는 정보
컴퓨터의 네 가지 핵심 부품
컴퓨터가 이해하는 정보
데이터
컴퓨터가 이해하는 숫자, 문자, 이미지, 동영상과 같은 정적인 정보
명령어
컴퓨터를 실직적으로 작동 시키는 중요한 정보
데이터 없이는 아무것도 할 수 없는 정보 덩어리
“데이터를 움직이고 컴퓨터를 작동 시키는 장보”
“즉, 명령어는 컴퓨터를 작동시키는 정보이고, 데이터는 명령어를 위해 존재하는 일종의 재료입니다.”
컴퓨터 프로그램은 ‘명령어들의 모음’으로 정의되기도 합니다.
그래서 명령어는 컴퓨터 구조를 학습하는 데 있어 데이터보다 더 중요한 개념.
컴퓨터의 4가지 핵심 부품.
중앙처리장치(Central Programming Unit, CPU)
컴퓨터의 두뇌
메모리에 저장된 명령어를 읽어 들이고, 읽어 들인 명령어를 해석하고, 실행하는 부품입니다.
CPU 내부 구성 요소 중 가장 중요한 세 가지는 산술논리연산장치(ALU: Arithmetic Logic Unit), 레지스터(register), 제어장치(CU: Control Unit) 입니다.
ALU: 계산기, 계산만을 위해 존재하는 부품, 컴퓨터 내부에서 수행되는 대부분의 계산은 ALU가 도맡아 수행
레지스터: CPU 내부의 작은 임시 저장 장치, 프로그램을 실행하는 데 필요한 값들을 임시로 저장, CPU 안에는 여러 개의 레지스터가 존재하고 각기 다른 이름과 역할을 가짐
제어장치: 제어 신호(Control Signal)라는 전기 신호를 내보내고 명령어를 해석하는 장치.
제어 신호란 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호
CPU가 메모리에 저장된 값을 읽고 싶을 땐 메모리를 향해 “메모리 읽기”라는 제어 신호를 보낸다.
CPU가 메모리에 어떤 값을 저장하고 싶을 땐 메모리를 향해 “메모리 쓰기”라는 제어 신호를 보낸다.
주기억장치(Main memory, 메모리)
현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품.
즉, 프로그램이 실행되려면 반드시 메모리에 저장되어 있어야 합니다.
메모리에 저장된 값의 위치는 주소로 알 수 있습니다.
보조기억장치(secondary storage)
메모리보다 크기가 크고 전원이 꺼져도 저장된 내용을 잃지 않는 메모리를 보조할 저장 장치
보조기억장치는 ‘보관할’ 프로그램을 저장한다고 생각해도 좋다.
입출력장치(input/output(I/O) device)
마이크, 스피커, 프린터, 마우스, 키보드처럼 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환하는 장치를 의미.
‘컴퓨터 주변에 붙어 있는 장치’라는 의미에서 “주변장치(peripheral device)”라 통칭하기도 함.
“주소”
컴퓨터가 빠르게 작동하기 위해서는 메모리 속 명령어와 데이터가 정돈된 위치에 저장되어 있어야 합니다.
그래서 메모리에는 저장된 값에 빠르게 효율적으로 접근하기 위해 주소(address)라는 개념이 사용됩니다.
주소로 메모리 내 원하는 위치에 접근할 수 있습니다.
메인보드와 시스템 버스
메인보드
마더보드(mother board)라고도 부름
메인보드에는 앞에서 소개한 부품을 비롯한 여러 컴퓨터 부품을 부착할 수 있는 슬록과 연결 단자가 있습니다.
메인 보드에 연력된 부품들은 서로 정보를 주고 받을수 있습니다. 이는 메인보드 내부에 “버스(bus)”라는 통로가 있기 때문입니다.
시스템 버스(system bus)
여러 버스 가운데 컴퓨터의 네 가지 핵심 부품을 연결하는 가장 중요한 버스입니다.
주소 버스, 데이터 버스, 제어 버스로 구성되어 있습니다.
주소 버스(address bus): 주소를 주고받는 통로
데이터 버스(data bus): 명령어롸 데이터를 주고 받는 통로
제어 버스(control bus): 제어 신호를 주고 받는 통로
키워드로 정리하는 핵심 포인트
컴퓨터가 이해하는 정보에는 “데이터” 와 “명령어” 가 있습니다.
“메모리” 는 현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품입니다.
“CPU” 는 메모리에 저장된 명령어를 읽어 들이고, 해석하고, 실행하는 부품입니다.
“보조기억장치” 는 전원이 꺼져도 보관할 프로그램을 저장하는 부품입니다.
“입출력장치” 는 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환할 수 있는 부품입니다.
“시스템 버스” 는 컴퓨터의 네 가지 핵심 부품들이 서로 정보를 주고받는 통로입니다.
Q1. “메모리 주소가 무엇이며, iOS 시스템 내에서 어떤 역할을 수행한다고 생각하나요?”
메모리 주소는 컴퓨터 메모리 내에서 데이터나 명령어의 위치를 식별하는 데 사용되는 고유한 식별자입니다. 각 바이트 또는 워드에는 메모리 내의 위치를 나타내는 고유한 주소가 있으며, 이를 통해 CPU와 다른 시스템 구성 요소가 필요한 데이터를 정확히 찾아 읽고 쓸 수 있습니다.
iOS 시스템 내에서 메모리 주소의 역할은 특히 중요합니다. iOS는 메모리 관리에 자동 참조 카운팅(ARC)를 사용하여 객체의 생명 주기를 관리합니다. ARC는 객체에 대한 참조가 더 이상 필요하지 않게 되면 자동으로 메모리를 해제합니다. 이 과정에서 메모리 주소를 사용하여 각 객체의 위치를 파악하고 관리합니다. 따라서, 개발자로서 메모리 주소의 이해는 메모리 누수를 방지하고 앱의 성능을 최적화하는 데 필수적입니다.
또한, 메모리 주소를 이해하는 것은 포인터를 사용한 프로그래밍, 메모리 접근 최적화, 그리고 다양한 메모리 관리 기법을 적용하는 데 중요합니다. 예를 들어, 효율적인 데이터 구조 설계, 대규모 데이터 처리, 멀티스레딩 환경에서의 데이터 공유와 동기화 문제 해결 등은 메모리 주소와 밀접한 관련이 있습니다.
iOS 시스템 내에서 메모리 주소의 관리와 최적화는 앱의 반응 속도, 안정성, 그리고 사용자 경험에 직접적인 영향을 미치기 때문에, 이를 정확히 이해하고 효과적으로 활용하는 능력은 iOS 개발자에게 매우 중요한 자질입니다.
Q2. “메모리 주소가 무엇이며, Java 시스템 내에서 어떤 역할을 수행한다고 생각하나요?”
“메모리 주소는 컴퓨터 메모리 내의 특정 위치를 식별하는 데 사용되는 고유한 식별자입니다. 이 주소를 통해, 컴퓨터 시스템은 메모리 내에서 데이터나 명령어를 정확히 찾아내어 읽고 쓸 수 있습니다. 간단히 말해, 메모리 주소는 컴퓨터 메모리 내의 ‘우편 주소’와 유사한 역할을 수행합니다.
Java 시스템 내에서, 메모리 주소의 역할은 Java 가상 머신(JVM)에 의해 추상화되어 다루어집니다. Java 개발자들은 직접적으로 메모리 주소를 다루지 않으며, 대신 Java가 제공하는 추상화된 메모리 모델을 사용하여 프로그래밍합니다. Java에서는 객체와 배열 등이 힙 메모리에 할당되며, 개발자는 이러한 객체에 대한 참조를 통해 메모리를 접근하게 됩니다. 여기서 ‘참조’는 실제 메모리 주소를 직접적으로 나타내지는 않지만, 특정 객체를 가리키는 역할을 합니다.
JVM은 가비지 컬렉션(Garbage Collection)을 통해 메모리 관리를 자동화합니다. 가비지 컬렉터는 더 이상 사용되지 않는 객체를 자동으로 검출하고, 그 메모리를 회수하여 재사용 가능하게 만듭니다. 이 과정에서 JVM은 내부적으로 메모리 주소를 관리하여, 효율적인 메모리 할당과 해제를 수행합니다.
따라서, Java 시스템 내에서 메모리 주소는 주로 메모리 할당, 객체 참조, 그리고 가비지 컬렉션과 같은 메모리 관리 작업에 중요한 역할을 수행합니다. Java 개발자로서 우리의 역할은 주로 안전하고 효율적인 코드 작성에 초점을 맞추며, JVM이 메모리 관리의 세부 사항을 추상화하고 처리하도록 합니다. 이렇게 함으로써, 개발자는 메모리 관리의 복잡성으로부터 벗어나 비즈니스 로직 구현에 더 집중할 수 있습니다.”
-
-
-
-
☕️[Java] 다운캐스팅과 주의점
다운캐스팅과 주의점.
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있습니다.
다음 코드를 통해 다운캐스팅에서 발생할 수 있는 문제를 확인해봅시다.
package poly.basic;
// 다운캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가
}
}
실행 결과
Child.childMethod
Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child (poly.basic.Parent and poly.basic.Child are in unnamed module of loader 'app')
at poly.basic.CastingMain4.main(CastingMain4.java:12)
실행 결과를 보면 child1.childMethod()는 잘 호출되었지만, child2.childMethod()는 실행되지 못하고, 그 전에 오류가 발생합니다.
예제의 parent1의 경우 다운캐스팅을 해도 문제가 되지 않습니다.
예제의 parent2를 다운캐스팅하면 ClassCastException 이라는 심각한 런타임 오류가 발생합니다.
이 코드를 자세히 알아봅시다.
Parent parent2 = new Parent()
먼저 new Parent()로 부모 타입으로 객체를 생성합니다.
따라서 메모리 상에 자식 타입은 전혀 존재하지 않습니다.
생성 결과를 parent2에 담아둡니다.
이 경우 같은 타입이므로 여기서는 문제가 발생하지 않습니다.
Child child2 = (Child) parent2
다음으로 parent2를 Child 타입으로 다운캐스팅합니다.
그런데 parent2는 Parent로 생성되었습니다.
따라서 메모리상에 Child 자체가 존재하지 않습니다. Child 자체를 사용할 수 없는 것입니다.
자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastExecption이라는 예외를 발생시킵니다.
예외가 발생하면 다음 동작이 실행되지 않고, 프로그램이 종료됩니다.
따라서 child2.childMethod() 코드 자체가 실행되지 않습니다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅의 경우 이런 문제가 절대로 발생하지 않습니다.
왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성되기 때문입니다!
따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전합니다.
따라서 캐스팅을 생략할 수 있습니다.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있습니다.
왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않습니다.
따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 합니다.
클래스 A, B, C는 상속 관계입니다.
new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성됩니다.
따라서 C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조할 수 있습니다.
“상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.”
A a = new C(): A로 업캐스팅
B b = new C(): B로 업캐스팅
C c = new C(): 자신과 같은 타입
new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성됩니다.
따라서 B의 부모 타입인 A, B 모두 B 인스턴스를 참조 할 수 있습니다.
상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.
하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있습니다.
A a = new B() : A로 업캐스팅
B b = new B() : 자신과 같은 타입
C c = new B() : 하위 타입은 대입할 수 없음, 컴파일 오류
C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생
컴파일 오류 vs 런타임 오류
컴파일 오류는 변수명 오류, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류입니다.
이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류 입니다.
반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류입니다.
런타임 오류는 매우 안좋은 오류입니다.
왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문입니다.
-
-
-
💾 [CS] 패턴 매칭(Pattern Matching)과 표현 매칭(Expression Matching)
패턴 매칭(Pattern Matching)과 표현 매칭(Expression Matching).
패턴 매칭과 표현 매칭은 프로그래밍 언어나 소프트웨어 개발에서 사용되는 두 가지 다른 개념입니다.
둘 다 데이터나 표현식의 구조를 분석하고 일치 여부를 판단하는 방법이지만, 적용되는 맥락과 목적에서 차이가 있습니다.
패턴 매칭(Pattern Matching).
데이터의 구조와 그 내용을 기반으로 한 매칭 방식입니다.
입력된 데이터가 특정 패턴이나 구조와 일치하는지를 검사합니다.
이를 통해 데이터의 타입, 값, 구조 등을 확인하고 , 그에 따른 처리를 분기하는 데 사용됩니다.
표현 매칭(Expression Matching)
특정 표현식이나 문자열이 주어진 패턴이나 규칙과 일치하는지를 확인하는 방법입니다.
주로 문자열 처리, 정규 표현식 사용, 텍스트 분석에서 널리 사용됩니다.
표현 매칭은 특정 패턴(예: 정규 표현식)을 정의하고, 대상 문자열이 이 패턴과 일치하는지 여부를 판단합니다.
이는 검색, 데이터 검증, 파싱 등 다양한 분야에서 활용됩니다.
차이점 요약
적용 분야
패턴 매칭은 주로 데이터의 구조와 타입을 다루는 함수형 프로그래밍에서 사용됩니다.
반면, 표현 매칭은 문자열이나 텍스트 데이터를 처리할 때 사용되는 패턴(예: 정규 표현식)과의 일치 여부를 확인하는 데 쓰입니다.
목적
패턴 매칭은 데이터의 구조를 통해 복잡한 데이터 타입을 효율적으로 분해하고 처리하는 데 중점을 둡니다.
표현 매칭은 문자열 내에서 특정 패턴의 존재 여부를 검사하고, 데이터를 검증하거나 추출하는 데 주로 사용됩니다.
사용 사례
패턴 매칭은 데이터 타입 분해, 조건 분기 처리 등에 사용되며, 함수형 프로그래밍 언어에서 자주 볼 수 있습니다.
표현 매칭은 로그 분석, 웹 페이지 파싱, 사용자 입력 검증 등 문자열 처리에 널리 사용됩니다.
두 방법은 각각의 사용 사례와 목적에 맞게 선택하여 사용되며, 프로그래밍에서의 다양한 문제를 해결하는 데 중요한 역할을 합니다.
-
💾 [CS] 컴퓨터 구조를 알아야 하는 이유
컴퓨터 구조를 알아야 하는 이유.
컴퓨터 구조는 실력 있는 개발자가 되려면 반드시 알아야 할 기본 지식입니다
why?
문제 해결
컴퓨터 구조를 이해하고 있다면 문제 상황을 빠르게 진단할 수 있고, 문제 해결의 실마리를 다양하게 찾을 수 있습니다.
컴퓨터 구조 지식은 다양한 문제를 스스로 해결할 줄 아는 개발자로 만들어 줍니다.
성능, 용량, 비용
“컴퓨터 구조애서 배우는 내용은 결국 성능, 용량, 비용과 직결됩니다.”
즉, 컴퓨터 구조를 이해하면 입력과 출력에만 집중하는 개발을 넘어 성능, 용량, 비용까지 고려하며 개발하는 개발자가 될 수 있습니다,
문제 해결
컴퓨터 구조를 이해하고 있다면 문제 상황을 빠르게 진단할 수 있고, 문제 해결의 실마리를 다양하게 찾을 수 있습니다.
컴퓨터 내부를 거리낌 없이 들여다보면 더 좋은 해결책을 고민할 수 있습니다.
이러한 사고가 가능한 이들에게 컴퓨터란 ‘미지의 대상’이 아닌 ‘분석의 대상’이기 때문입니다.
컴퓨터 구조 지식은 다양한 문제를 스스로 해결할 줄 아는 개발자로 만들어 줍니다.
성능, 용량, 비용
성능, 용량, 비용 문제는 프로그래밍 언어의 문법만 알아서는 해결하기 어렵습니다.
혼자만 사용하는 프로그램을 만들 떄는 이러한 문제를 생각조차 해 본 적이 없을 수도 있습니다.
하지만 유튜브, 워드, 포토샵과 같이 사용자가 많은 프로그램은 필연적으로 성능, 용량, 비용이 고려됩니다.
그래서 컴퓨터 구조를 아는 것은 매우 중요합니다.
“컴퓨터 구조애서 배우는 내용은 결국 성능, 용량, 비용과 직결됩니다.”
즉, 컴퓨터 구조를 이해하면 입력과 출력에만 집중하는 개발을 넘어 성능, 용량, 비용까지 고려하며 개발하는 개발자가 될 수 있습니다,
핵심 포인트
컴퓨터 구조를 이해하면 “문제 해결” 능력이 향상 됩니다.
컴퓨터 구조를 이해하면 문법만으로는 알기 어려운 “성능/용량/비용” 을 고려하며 개발할 수 있습니다.
Q1. iOS 애플리케이션 개발에서 고효율적이고 성능이 우수한 앱을 만들기 위해 컴퓨터 구조에 대한 이해가 왜 중요한지 설명해주세요. 구체적인 예를 들어서 설명해주시기 바랍니다.
답변.
iOS 애플리케이션 개발에서 컴퓨터 구조에 대한 이해는 여러 면에서 중요합니다.
첫째, 성능 최적와레 있어서 핵심적인 역할을 합니다.
예를 들어, CPU의 멀티코어 구조를 이해함으로써, 병렬 처리와 동시성을 통해 애플리케이션의 성능을 효과적으로 향상시킬 수 있습니다.
이는 앱이 사용자의 입력에 빠르게 반응하고, 더 복잡한 작업을 빠른 시간 안에 처리할 수 있게 만들어 줍니다.
둘째, 메모리 관리에 대한 이해를 통해, 애플리케이션의 효율성을 높일 수 있습니다.
예를 들어, RAM과 캐시의 작동 방식을 이해하면, 데이터를 효율적으로 저장하고 접근하는 방법을 개선할 수 있으며, 이는 애플리케이션의 반응 속도와 전반적인 성능에 긍정적인 영향을 미칩니다.
셋째, 하드웨어와 소프트웨어의 상호작용에 대한 이해는 에너지 효율성을 최적화하는 데 도움이 됩니다.
iOS 장치의 배터리 수명은 사용자 경험의 중요한 부분이며, 컴퓨터 구조에 대한 이해는 개발자가 배터리 소모를 최소화하면서 성능을 극대화할 수 있는 애플리케이션을 설계할 수 있게 돕습니다.
마지막으로 컴퓨터 구조에 대한 깊은 이해는 개발자가 효과적인 코드를 작성하고, 시스템 리소스를 효율적으로 관리하며, 최종 사용자에게 더 나은 경험을 제공할 수 있는 애플리케이션을 만들 수 있도록 합니다.
Q2. 서버 개발자가 컴퓨터 구조에 대한 기본적인 지식을 갖추어야 하는 이유에 대해 설명해주시고, 그 지식이 어떻게 서버 애플리케이션의 성능과 안정성에 영향을 미칠 수 있는지 구체적인 예시를 들어 설명해주세요.
답변.
서버 개발자에게 컴퓨터 구조에 대한 이해는 애플리케이션의 성능 최적화와 안정성 보장에 필수적입니다.
첫 번째 이유는 성능 최적화와 관련이 있습니다.
예를 들어, CPU의 멀티코어 아키텍처를 이해함으로써 서버 애플리케이션에서 멀티 스레딩과 병렬 처리를 효율적으로 구현할 수 있습니다.
이는 요청 처리량을 증가시키고 응답 시간을 단축시킬 수 있으며, 고객에게 더 나은 서비스 경험을 제공할 수 있습니다.
두 번째 이유는 자원 관리와 관련이 있습니다.
서버 애플리케이션은 종종 대량의 데이터를 처리하고, 메모리 및 CPU 자원을 집중적으로 사용합니다.
메모리 계층 구조(예: 캐시, 주 메모리)와 이에 대한 이해는 데이터 접근 시간을 최적화하고, 메모리 사용 효율을 극대화하는 방법을 개발자에게 제공합니다.
이를 통해 시스템의 전반적인 효율성을 향상시킬 수 있습니다.
세 번째 이유는 안정성과 가용성에 있습니다.
서버 개발자는 컴퓨터 구조에 대한 이해를 통해 시스템의 잠재적 한계와 병목 현상을 더 잘 파악할 수 있으며, 이를 바탕으로 더 견고하고 오류에 강한 시스템을 설계할 수 있습니다.
예를 들어, 서버 하드웨어의 장애 지점을 이해하고, 이에 대비한 높은 가용성을 보장하는 소프트웨어 설계를 할 수 있습니다.
종합하면, 컴퓨터 구조에 대한 깊은 이해는 서버 개발자가 성능, 자원 관리, 안정성을 고려한 효율적이고 안정적인 서버 애플리케이션을 설계하고 구현하는 데 있어 필수적입니다.
이는 최종적으로 사용자 경험을 개선하고, 비즈니스 목표 달성에 기여합니다.
-
-
-
-
☕️[Java] 상속과 접근 제어
상속과 접근 제어.
상속 관계와 접근 제어에 대해 알아보겠습니다.
참고로 접근 제어를 자세히 설명하기 위해 부모와 자식의 패키지를 따로 분리하였습니다.
접근 제어자를 표현하기 위해 UML 표기법을 일부 사용했습니다.
+: public
#: protected
~: default
-: private
접근 제어자를 잠시 복습해봅시다.
접근 제어자의 종류
private: 모든 외부 호출을 막습니다.
default(package-private): 같은 패키지 안에서 호출은 허용합니다.
protected: 같은 패키지 안에서 호출은 허용합니다.
패키지가 달라도 상속 관계의 호출은 허용합니다.
public: 모든 외부 호출을 허용합니다.
순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용합니다.
private -> default -> protected -> public
그림과 같이 다양한 접근 제어자를 사용하도록 코드를 작성해보겠습니다.
Parent
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
ExtendsAccessMain
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
Parent와 Child의 패키지가 다르다는 부분에 유의합시다.
자식 클래스인 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는지 확인해봅시다.
publicValue = 1;: 부모의 public 필드에 접근합니다. public이므로 접근할 수 있습니다.
protectedValue = 1;: 부모의 protected 필드에 접근합니다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있습니다.
defaultValue = 1;: 부모의 default 필드에 접근합니다. 자식과 부모가 다른 패키지이므로 접근할 수 없습니다.
privateValue = 1;: 부모의 private 필드에 접근합니다. private은 모두 외부 접근을 막으므로 자식이라도 호출할 수 없습니다.
메서드의 경우도 앞서 설명한 필드와 동일합니다.
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
코드를 실행해보면 Child.call() -> Parent.printParent() 순서로 호출합니다.
Child는 부모의 public, protexted 필드나 메서드만 접근할 수 있습니다.
반면에 Parent.printParent()의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있습니다.
접근 제어와 메모리 구조.
본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 줍니다.
왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문입니다.
결국 자식 차입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같습니다.
-
☕️[Java] 상속과 메서드 오버라이딩
상속과 메서드 오버라이딩.
부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있습니다.
예를 들어서 자동차의 경우 Car.move()라는 기능이 있습니다.
이 기능을 사용하면 단순히 “차를 이동합니다.” 라고 출력합니다.
전기차의 경우 보통 더 빠르기 때문에 전기차가 move()를 호출한 경우에는 “전기차를 빠르게 이동합니다.”라고 출력을 변경하고 싶습니다.
이렇게 부모에게서 상속 받은 기능을 자식이 재정의 하는 것을 “메서드 오버라이딩(Overriding)” 이라고 합니다.
package extends1.overriding - Car
package extends1.overriding;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
기존 코드와 같습니다.
package extends1.overriding - GasCar
package extends1.overriding;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.overriding - HydrogenCar
package extends1.overriding;
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
package extends1.overriding - ElectricCar
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않습니다.
메서드 이름은 같지만 새로운 기능을 사용하고 싶습니다.
그래서 ElectricCar의 move() 메서드를 새로 만들었습니다.
이렇게 부모의 기능을 자식이 새로 재정의하는 것을 “매서드 오버라이딩” 이라고 합니다.
이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 호출됩니다.
@Override
@이 붙은 부분을 애노테이션(어노테이션, annotattion)이라 합니다.
애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 됩니다.
이 애노테이션은 상위 클래스의 매서드를 오버라이드하는 것임을 나타냅니다.
이름 그대로 오버라이딩한 매서드 위에 이 애노테이션을 붙여야 합니다.
컴파일러는 이 애노테이션을 보고 매서드가 정확히 오버라이드 되었는지 확인합니다.
오바라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킵니다.
따라서 실수로 오버라이딩을 못하는 경우를 방지해줍니다.
예를 들어서 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생합니다.
참고로 이 기능은 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋습니다.
package extends1.overrding - CarMain
package extends1.overriding;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
GasCar gasCar = new GasCar();
gasCar.move();
}
}
실행 결과
전기차를 빠르게 이동합니다.
차를 이동합니다.
실행 결과를 보면 electricCar.move()를 호출했을 때 오버라이딩한 ElectricCar.move() 메서드가 실행된 것을 확인할 수 있습니다.
오버라이딩과 클래스
Car의 move() 메서드를 ElectricCar에서 오버라이딩 했습니다.
오버라이딩과 메모리 구조
electricCar.move()를 호출합니다.
호출한 electricCar의 타입은 ElectricCar입니다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작합니다.
ElectricCar 타입에 move() 메서드가 있습니다. 해당 메서드를 실행합니다. 이때 실행할 메서드를 이미 찾았으므로 부모 타입을 찾지 않스빈다.
오버로딩(Overloading)과 오버라이딩(Overriding)
메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라고 합니다.
오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻입니다.
따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 됩니다.
메서드 오버라이딩: 메서드 오버라이딩은 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미합니다.
따라서 상속 관계에서 사용합니다. 부모의 기능을 자식이 다시 정의하는 것입니다.
오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것을 말합니다.
자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 됩니다.
오버라이딩을 번역하면 무언가를 다시 정의한다고 해서 재정의라 합니다.
상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 됩니다.
실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용합니다.
메서드 오버라이딩 조건
메서드 오버라이딩은 다음과 같은 까다로운 조건을 가지고 있습니다.
다음 내용은 아직 학습하지 않은 내용들도 있으므로 이해하려고 하기 보다는 참고만 합시다.
지금은 단순히 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분합니다.
메서드 오버라이딩 조건
메서드 이름: 메서드 이름이 같아야 합니다.
메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 합니다.
반환 타입: 반환 타입이 같아야 합니다. 단, 반환 타입이 하위 클래스 타입일 수 있습니다.
접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안됩니다.
예를 들어, 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드 할 수 있지만, private 또는 default로 오버라이드 할 수 없습니다.
예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없습니다.
하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있습니다.
static, final, private: 키워드가 붙은 메서드는 오버라이딩 될 수 없습니다.
static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없습니다.
쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됩니다.
final 메서드는 재정의를 금지합니다.
private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않습니다.
따라서 오버라이딩 할 수 없습니다.
생성자 오버라이딩: 생성자는 오버라이딩 할 수 없습니다.
-
-
-
-
-
-
-
-
-
🆙[Cpp DataStructure] 안정성(stability) 확인
안정성(stability) 확인.
#include <iostream>
#include <cassert>
#include <fstream>
using namespace std;
struct Element
{
int key;
char value;
};
void Print(Element* arr, int size)
{
for (int i = 0; i < size; i++)
cout << arr[i].key << " ";
cout << endl;
for (int i = 0; i < size; i++)
cout << arr[i].value << " ";
cout << endl;
}
int main()
{
// 안정성 확인(unstable)
{
Element arr[] = { {2, 'a'}, {2, 'b'}, {1, 'c' } };
int size = sizeof(arr) / sizeof(arr[0]);
Print(arr, size);
int min_index;
for(int i = 0; i < (size - 1); i++)
{
min_index = i;
for (int j = (i + 1); j < size; j++)
{
if (arr[j].key < arr[min_index].key)
{
min_index = j;
}
}
swap(arr[i], arr[min_index]);
Print(arr, size);
}
}
return 0;
}
실행 결과
2 2 1
a b c
1 2 2
c b a
1 2 2
c b a
정렬의 안정성(stablity)이라는 개념이 있습니다.
이 개념은 stable한지 unstable한지로 구분합니다.
즉, 안정적인지 불안정적인지로 구분합니다.
위 코드에서 보면 Element 배열을 정렬합니다.
이 Èlement를 보면 다음과 같습니다.
struct Element
{
int key;
char value;
};
Element의 key는 정수이고 value는 문자입니다.
그래서 정렬할 때 key를 기준으로 정렬합니다.
그러면 swap시 value(문자)는 함께 따라갑니다. -> 복사를 하니 따라가는 것 입니다.
Element arr[] = { {2, 'a'}, {2, 'b'}, {1, 'c' } };
첫 번째 인덱스의 요소의 key는 2이고 value는 'a' 입니다.
두 번째 인덱스의 요소의 key는 2이고 value는 'b' 입니다.
세 번째 인덱스의 요소의 key는 1이고 value는 'c' 입니다.
“정렬시에는 key가 작은 순서대로 정렬되도록 할 것 입니다.”
즉, key를 기준으로 정렬합니다.
여기서 주목해야 할 점은 “첫 번째 인덱스와 두 번째 인덱스의 요소의 Value”입니다.
두 인덱스의 요소의 key 값은 같으나 value 값은 다릅니다.
비교 로직을 보면 key값을 가지고 비교를 하지만 swap 시에는 key와 value를 모두 가지고 움직입니다.
실행 결과를 보고 value와 안정성의 상관 관계에 대해 알아봅시다.
실행 결과
2 2 1
a b c
1 2 2
c b a
1 2 2
c b a
실행 결과를 보면 (2 a), (2, b), (1, c)에서 “(1, c), (2, b), (2, a)” 순으로 정렬된 것을 볼 수 있습니다.
처음에는 (2 a), (2, b) 순서였지만 정렬 후에는 “(2, b), (2, a)” 로 순서가 바뀌어 정렬되었습니다.
value가 뒤바뀐 것을 알 수 있습니다.
key가 정렬이 되었으므로 정렬은 잘 되었다고 볼 수 있습니다.
그러나 “key가 같은 값일 경우에는 value의 순서가 a, b에서 b, a 로 바뀌었습니다.”
“여기서 stable한 것과 unstable한 것의 차이점이 나타납니다.”
“키 값이 같아도 정렬시 벨류 값이 처음과 같이 유지가 된다면 그것은 stable하다라고 합니다, 그와 반대로 키 값이 같으나 처음과 달리 벨류의 순서가 바뀔 경우에는 unstable하다 라고 합니다.”
이것도 정렬 알고리즘을 분류하는 기준 중 하나입니다.
-
-
☕️[Java] static 변수3
static 변수 3
이번에는 static 변수를 정리해보겠습니다.
용어 정리
public class Data3 {
public String name;
public static int count; // static
}
예제 코드에서 name, count는 둘 다 멤버 변수입니다.
멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류 할 수 있습니다.
멤버 변수(필드)의 종류.
인스턴스 변수: static이 붙지 않은 멤버 변수, 예) name
static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다.
따라서 인스턴스 변수라 합니다.
인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어집니다.
클래스 변수: static이 붙은 멤버 변수, 예) count
클래스 변수, 정적 변수, static 변수등으로 부릅니다. 용어를 모두 사용하니 주의합시다.
static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있습니다.
따라서 클래스 변수라 합니다.
클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어집니다.
인스턴스와는 다른게 보통 여러곳에서 공유하는 목적으로 사용됩니다.
변수와 생명주기
지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관됩니다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됩니다.
따라서 지역 변수는 생존 주기가 짧습니다.
인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 합니다. 인스턴스 변수는 힙 영역을 사용합니다.
힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지 생존하기 때문에 보통 지역 변수보다 생존 주기가 깁니다.
클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관 되는 변수입니다.
메서드 영역은 프로그램 전체에서 사용하는 공용 공간입니다.
클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성됩니다.
그리고 JVM이 종료될 때까지 생명주기가 이어집니다.
따라서 가장 긴 생명주기를 가집니다.
static이 정적이라는 이유는 바로 여기에 있습니다.
힙 역역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거됩니다.
반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다.
정적 변수는 이름 그대로 정적입니다.
정적 변수 접근 법
static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다.
DataCountMain3 마지막 코드에 다음 부분을 추가하고 실행해보겠습니다.
DataCountMain3 - 추가
// 추가
// 인스턴스를 통한 접근
Data3 data4 = new Data3("D");
// 클래스를 통합 접근
System.out.println(Data3.count);
실행 결과 - 추가된 부분
4
4
둘 다 차이는 없습니다. 둘다 결과적으로 정적 변수에 접근합니다.
인스턴스를 통한 접근 data4.count
정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않습니다.
왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문입니다.
클래스를 통한 접근 Data3.count
정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확합니다.
따라서 정적 변수에 접근할 때는 클래스를 통해서 접근합시다.
-
-
☕️[Java] static 메서드 2
static 메서드 2
정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있습니다.
하지만 정적 메서드는 언제나 사용할 수 있는 것이 아닙니다.
정적 메서드 사용법
static 메서드는 static만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없습니다.
반대로 모든 곳에서 static을 호출할 수 있습니다.
정적 메서드는 공용 기능입니다.
따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있습니다.
예제를 통해 정적 메서드의 사용법을 확인해봅시다.
DecoData
package static2;
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
//instanceValue++; // 인스턴스 변수 접근, compile error
//instanceMethod(); // 인스턴스 메서드 접근, compile error
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
public void instanceCall() {
instanceValue++; // 인스턴스 변수 접근
instanceMethod(); // 인스턴스 메서드 접근
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue);
}
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
이번 예제에서는 접근 제어자를 적극 활용해서 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었습니다.
instanceValue는 인스턴스 변수입니다.
staticValue는 정적 변수(클래스 변수)입니다.
instanceMethod()는 인스턴스 메서드입니다.
staticMethod()는 정적 메서드(클래스 메서드)입니다.
staticCall() 메서드를 봐봅시다.
이 메서드는 정적 메서드입니다.
따라서 static 만 사용할 수 있습니다.
정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생합니다.
코드를 보면 staticCall() -> staticMethod()로 static에서 static을 호출하는 것을 확인할 수 있습니다.
instanceCall() 메서드를 봐봅시다.
이 메서드는 인스턴스 메서드입니다.
모든 곳에서 공용인 static을 호출할 수 있습니다.
따라서 정적 변수, 정적 메서드에 접근할 수 있습니다.
물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있습니다.
DecoDataMain
package static2;
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
DecoData.staticCall();
System.out.println("2. 인스턴스 호출1");
DecoData data1 = new DecoData();
data1.instanceCall();
System.out.println("3. 인스턴스 호출2");
DecoData data2 = new DecoData();
data2.instanceCall();
}
}
실행 결과
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출2
instanceValue=1
staticValue=3
정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있습니다.
그래서 인스턴스처럼 참조값의 개념이 없습니다.
특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출합니다.
따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없습니다.
물론 당연한 이야기지만 다음과 같이 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있습니다.
public static void staticCall(DecoData data) {
data.instanceValue++;
data.instanceMethod();
}
-
-
-
-
-
-
-
☕️[Java] 자바 메모리 구조
자바 메모리 구조.
자바 메모리 구조 - 비유
자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역, 3개로 나눌 수 있습니다.
메서드 영역: 클래스 정보를 보관합니다. 이 클래스 정보가 붕어빵 틀입니다.
스택 영역: 실제 프로그램이 실행되는 영역입니다. 메서드를 실행할 때 마다 하나씩 쌓입니다.
힙 영역: 객체(인스턴스)가 생성되는 영역입니다. new 명령어를 사용하면 이 영역을 사용합니다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간입니다.
참고로 배열도 이 영역에 생성됩니다.
위 설명한 내용은 쉽게 비유로 한 것이고 실제는 다음과 같습니다.
메서드 영역(Method Are): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리합니다. 이영역은 프로그램의 모든 영역에서 공유합니다.
클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재합니다.
static영역: static 변수들을 보관합니다.
런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관합니다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리합니다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리합니다.
스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성됩니다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함합니다.
스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임입니다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거됩니다.
힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역입니다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됩니다.
참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성됩니다. 따라서 쓰레드 수 만큼 스택 영역이 생성됩니다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나입니다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있습니다.
메서드 코드는 메서드 영역에
자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생깁니다.
각각의 인스턴스는 내부에 변수와 메서드를 가집니다.
같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유합니다.
따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없습니다.
메서드는 메서드 영역에서 공통으로 관리되고 실행됩니다.
정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행합니다.
-
☕️[Java] 캡슐화
캡슐화
캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다.
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말합니다.
캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있습니다.
캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 너머지는 모두 내부로 숨기는 것입니다.
이전에 객체 지향 프로그래밍을 설명하면서 캡슐화에 대해 알아보았습니다.
이때는 데이터와 데이터를 처리하는 메서드를 하나로 모으는 것에 초점을 맞추었습니다.
여기서 한발짝 더 나아가 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 “접근 제어자” 입니다.
그럼 어떤 것을 숨기고 어떤 것을 노출해야 할까요?
1. 데이터를 숨겨라.
객체에는 속성(데이터)과 기능(메서드)이 있습니다.
캡슐화에서 가장 필수로 숨겨야 하는 것은 “속성(데이터)” 입니다.
package access;
public class Speaker {
private int volume;
Speaker(int volume) {
this.volume = volume;
}
void volumeUp() {
if (volume >= 100) {
System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
} else {
volume += 10;
System.out.println("음량을 증가합니다.");
}
}
void volumeDown() {
volume -= 10;
System.out.println("volumeDown 호출");
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
위 코드에서의 Speaker의 volume을 봐봅시다.
객체 내부의 데이터를 외부에서 함부로 접근하게 둔다면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있습니다.
결국 모든 안전망을 다 빠져나가게 됩니다.
따라서 캡슐화가 깨집니다.
우리가 자동자를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않습니다.
단지 자동차가 제공하는 액셀 기능을 사용해서 액셀을 밟으면 자동차가 나머지는 다 알아서 하는 것입니다.
우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려봅시다.
음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나, 볼륨 상태의 데이터를 직접 수정할 일이 있을까요?
우리는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지는 않습니다.
전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일입니다.
볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일입니다.
쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것입니다.
복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아닙니다.
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 합니다.
2. 기능을 숨겨라.
“객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있습니다.”
이런 기능도 모두 감추는 것이 좋습니다.
우리가 자동차를 운정하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리는 알 필요가 없습니다.
우리는 단지 렉셀과 핸들 정도의 기능만 알면 됩니다.
만약 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 합니다.
“사용자 입장에서 꼭 필요한 기능만 외부에 노출합시다. 나머지 기능은 모두 내부로 숨깁시다.”
“정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화입니다.”
이번에는 잘 캡슐화된 예제를 하나 만들어봅시다.
BankAccount
package access;
public class BankAccount {
private int balance;
public BankAccount() {
balance = 0;
}
// public 메서드: deposit
public void deposit(int amount) {
if (isAmountValid(amount)) {
balance += amount;
System.out.println(amount + "원이 입금되었습니다.");
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
// public 메서드: withdraw
public void withdraw(int amount) {
if (isAmountValid(amount) && (balance - amount >= 0)) {
balance -= amount;
System.out.println(amount + "원이 출금되었습니다.");
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드: getBalance
public int getBalance() {
return balance;
}
private boolean isAmountValid(int amount) {
// 금액이 0보다 커야함.
return amount > 0;
}
}
BankAccountMain
package access;
public class BankAccountMain {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(10000);
account.withdraw(3000);
System.out.println("balance = " + account.getBalance());
}
}
위 코드는 은행 계좌 기능을 다룹니다.
위 코드는 다음과 같은 기능을 가지고 있습니다.
private
balance : 데이터 필드는 외부에 직접 노출하지 않습니다. BankAccount가 제공하는 메서드를 통해서만 접근할 수 있슷빈다.
isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요한 기능입니다. 따라서 private을 사용했습니다.
pulbic
deposit() : 입금
withdraw() : 출금
getBalance() : 잔고
BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 됩니다.
나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있습니다.
“만약 isAmountValid()를 외부에 노출할 경우 어떻게될까요?”
BankAccount를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었습니다.
여러분이 BankAccount를 사용하는 개발자라면 어떤 생각을 할까요?
아마도 입금과 출금 전에 본인이 먼저 isAmountValid()를 사용해서 검증을 해야 하나? 라고 의문을 가질 것입니다.
“만약 balance 필드를 외부에 노출하면 어떻게 될까요?”
BankAccount를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있습니다.
“왜냐하면 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기 때문입니다.”
“결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있습니다.”
“접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있습니다.”
-
💾 [CS] 컴퓨터의 구성
컴퓨터의 구성.
1. 컴퓨터가 시스템은 크게 어떻게 나누어 지나요?
컴퓨터 시스템은 크게 하드웨어와 소프트웨어로 나누어집니다.
하드웨어는 컴퓨터를 구성하는 기계적 장치입니다.
중앙처리장치(CPU)
기억장치: RAM, HDD
입출력 장치: 마우스, 프린터
소프트웨어는 하드웨어의 동작을 지시하고 제어하는 명령어 집합입니다.
시스템 소프트웨어: 운영체제, 컴파일러
응용 소프트웨어: 워드프로세서, 스프레드시트
1.1 명령어란 무엇일까요?
명령어는 컴퓨터에게 무엇을, 어떻게 해야 하는지를 알려주는 지시사항입니다.
콤퓨터는 이러한 명령어들을 해석하고 실행하여 다양한 작업을 수행합니다.
명령어 구성 요소
연산자(Operation Code, Opcode) : 수행해야 할 기본적은 작업의 유형을 나타냅니다. 예를 들어, 데이터를 더하거나 빼거나, 저장하는 등의 작업이 이에 해당합니다.
피연산자(Operand) : 연산자가 작용할 데이터나, 그 데이터가 위치한 메모리 주소를 가리킵니다. 즉, 연산자가 어떤 데이터에 대한 작업을 수행할지를 지정합니다.
결과(Result) : 연산의 결과를 저장할 위치입니다. 이는 명령어에 따라 명시적으로 주어지거나, 특정 규칙에 따라 암시적으로 결정될 수 있습니다.
명령어들은 프로그래밍 언어로 작성되며, 고급 프로그래밍 언어에서 작성된 코드는 컴파일러나 인터프리터를 통해 기계어로 번역되어 컴퓨터가 이해할 수 있는 형태로 변환됩니다.
기계어는 컴퓨터의 프로세서가 직접 실행할 수 있는 매우 기본적이고 낮은 수준의 명령어 집합입니다.
2. 하드웨어란 무엇인가요?
하드웨어는 중앙처리장치(CPU), 기억장치, 입출력장치로 구성되어 있으며 이들은 시스템 버스로 연결되어 있습니다.
시스템 버스는 데이터와 명령 제어 신호를 각 장치로 실어나르는 역할을 합니다.
2.2 중앙처리장치(CPU)란 무엇인가요?
인간으로 따지면 두뇌에 해당하는 부분입니다.
주기억장치에서 프로그램 명령어와 데이터를 읽어와 처리하고 명령어의 수행 순서를 제어합니다.
중앙처리장치는 비교와 연산을 담당하는 산술논리연산장치(ALU)와 명령어의 해석과 실행을 담당하는 제어장치, 속도가 빠른 데이터 기억장소인 레지스터로 구성되어 있습니다.
개인용 컴퓨터와 같은 소형 컴퓨터에서는 CPU를 마이크로프로세서라고도 부릅니다.
2.3 기억장치란 무엇인가요?
프로그램, 데이터, 연산의 중간 결과를 저장하는 장치입니다.
기억장치는 주기억장치와 보조기억 장치로 나누어집니다.
RAM과 ROM도 이곳에 해당합니다.
실행중인 프로그램과 같은 프로그램에 필요한 데이터를 일시적으로 저장합니다.
보조기억장치는 하드디스크 등을 말하며, 주기억장치에 비해 속도는 느리지만 많은 자료를 영구적으로 보관할 수 있는 장점이 있습니다.
2.4 입출력장치란 무엇인가요?
먼저 입출력장치는 입력과 출력 장치로 나뉘어집니다.
입력 장치는 컴퓨터 내부로 자료를 입력하는 장치인 키보드, 마우스등이 이에 속합니다.
출력 장치는 컴퓨터에서 외부로 표현하는 장치인 프린터, 모니터, 스피커등이 이에 속합니다.
3. 시스템 버스란 무엇인가요?
시스템 버스는 하드웨어 구성 요소를 물리적으로 연결하는 선을 말합니다.
시스템 버스는 각 구성요소가 다른 구성요소로 데이터를 보낼 수 있도록 통로가 되어줍니다.
시스템 버스는 용도에 따라 데이터 버스, 주소 버스, 제어 버스로 나뉘어집니다.
3.1 데이터 버스란 무엇인가요?
데이터 버스란 중앙처리장치와 기타 장치 사이에서 데이터를 전달하는 통로를 말합니다.
기억장치와 입출력장치의 명령어와 데이터를 중앙처리장치로 보내거나, 중앙처리장치의 연산 결과를 기억장치와 입출력장치로 보내는 ‘양방향’ 버스입니다.
3.2 주소 버스란 무엇인가요?
주소 버스는 중앙처리장치가 주기억장치나 입출력장치로 기억장치 주소를 전달하는 통로입니다.
주소버스는 그렇기 때문에 ‘단방향’ 버스입니다.
데이터를 정확히 실어나르기 위해서는 기억장치’주소’를 정해주어야 합니다.
3.3 제어 버스
제어 버스는 중앙처리장치가 기억장치나 입출력장치에 제어 신호를 전달하는 통로입니다.
제어 신호의 종류에는 기억장치 읽기 및 쓰기, 버스 요청 및 승인, 인터럽트 요청 및 승인, 클락, 리셋 등이 있습니다.
제어 버스는 읽기 동작과 쓰기 동작을 모두 수행하기 때문에 ‘양방향’ 버스입니다.
제어 버스가 필요한 이유는 주소 버스와 데이터 버스는 모든 장치에 공유되는데 이때 이를 제어할 수단이 필요하기 때문입니다.
4. 컴퓨터의 데이터 처리과정
컴퓨터는 기본적으로 읽고 처리한 뒤 저장하는 과정으로 이루어집니다. (READ -> PROCESS -> WRITE)
이 과정을 진행하면서 끊임없이 주기억장치(RAM)과 소통합니다.
이때 운영체제가 64bit라면, CPU는 RAM으로부터 데이터를 한번에 64bit씩 읽어옵니다.
-
-
-
-
-
-
🆙[Cpp DataStructure] 교환(Swap)과 정렬(Sort)
Swap(교환)
먼저 아래의 코드를 보고 a와 b를 교환해봅시다.
#include <iostream>
using namespace std;
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
// TODO:
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
3 2
“TODO” 에는 어떤 코드가 들어가야 할까요?
“우리가 양 손에 사과🍎와 레몬🍋을 들고 있다고 생각해봅시다.”
그럼 왼손에는 사과🍎와 오른손에는 레몬🍋을 들고 있을 때 사과🍎와 레몬🍋을 바꾸려면 어떻게 해야할까요?
저는 하나의 접시를 가져와 그 접시에 사과 또는 레몬을 잠시 올려두고 비어있는 손으로 과일을 옮긴 뒤 접시에 있는 과일을 집을것 입니다.
그럼 코드도 똑같이 만들 수 있지 않을까요?
#include <iostream>
using namespace std;
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
// TODO:
// 먼저 a를 사과 b를 레몬이라고 생각하고,
// temp라는 접시를 만들어보겠습니다.
// 그 접시에 a라는 사과를 올려보겠습니다.
int temp = a;
// 그럼 비어있는 손으로 레몬을 옮길 수 있게되었네요.
// 비어있는 손으로 레몬을 옮겨보겠습니다.
// a가 있던 손으로 b를 옮깁니다.
a = b;
// 이번에는 b가 있던 손이 비었네요.
// b가 있던 손으로 접시(temp)에 있는 a를 들어보겠습니다.
temp = a;
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
양 손에 있던 사과(3)과 레몬(2)의 자리가 바뀌었습니다
“즉, 교환(Swap)이 이루어졌습니다.”
하지만 항상 이렇게 3줄의 라인인
int temp = a;
a = b;
b = temp;
위 코드처럼 코드를 만들어서 사용할 경우에는 매우 많은 교환(Swap)을 해야할 경우 코드의 양도 늘어나고 가독성도 좋지 않은 것 입니다.
“그럼 이번에는 함수를 이용해서 두 숫자를 교환해봅시다.”
먼저, 위 교환 코드를 그대로 가져다가 사용해볼까요?
void MySwap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
이 경우에는 리턴 값이 없기 때문에 불가능합니다.
만약 리턴값이 int 형이라고 해도 리턴은 1개만 가능하기 때문에 어렵습니다.
cpp의 다른 기능인 구조체나 여러 기능을 사용해야 할 것입니다.
그러면 어떻게 해야할까요?
“C의 포인터와 CPP의 레퍼런스를 활용하면됩니다.”
1. C의 포인터 활용
#include <iostream>
using namespace std;
void MySwapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
MySwapPtr(&a, &b);
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
실행 결과는 올바르게 나왔습니다.
“C 스타일의 포인터 사용은 *(별)을 사용하면 됩니다.”
void MySwapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
temp에 먼저 a의 주소값을 넣어 줍니다.
이후 a의 주소값에 b의 주소값을 넣어 줍니다.
b의 주소값에는 temp(a의 주소값)을 넣어줍니다.
“이렇게 되면 각 주소값이 교환이 됩니다.”
C 스타일의 단점은 선언과 호출시에 나타납니다.
선언시에는 위와 같이 *을 모두 붙여줘야 합니다.
호출시에는 아래와 같이 &를 붙여줘야 합니다.
MySwapPtr(&a, &b);
“하지만 CPP 스타일의 래퍼런스 교환은 단순합니다.”
// 선언시
void MySwapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 호출시
MySwapRef(a, b);
내부 구현은 똑같지만 별다른 어노테이션 없이 일반적인 변수 할당과 같이 해주면 래퍼런스 대입되고 교환이 이루어지는 것을 볼 수 있습니다.
호출시에도 어노테이션을 따로 붙일 필요없이 일반적인 매개변수(parameter)를 넣어주듯이 넣어주면 됩니다.
#include <iostream>
using namespace std;
void MySwapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main()
{
// Swap
{
int a = 3;
int b = 2;
cout << a << " " << b << endl;
MySwapRef(a, b);
cout << a << " " << b << endl;
}
return 0;
}
실행 결과
3 2
2 3
“이번에는 교환을 활용해서 정렬을 해보겠습니다.”
값과 상관 없이 항상 작은 값이 먼저 출력되게 하려면 어떻게 해야할까요?
즉, 두 값이 같을 때는 순서가 상관이 없지만 큰 값이 먼저 출력되지 않게 해야합니다.
먼저 두 값이 같지 않거나 큰 값이 먼저 출력 되었을 경우에 false를 출력하고 그와는 반대일 경우에는 true를 출력하는 코드를 작성해보겠습니다.
#include <iostream>
using namespace std;
// 정렬(sorting)
int main() {
int arr[2];
// TODO:
for (int j = 0; j < 5; j++) {
for (int i = 0; i < 5; i++) {
arr[0] = i;
arr[1] = j;
cout << boolalpha;
cout << arr[0] << " " << arr[1] << " "
<< (arr[0] <= arr[1]) << endl;
}
}
return 0;
}
먼저 배열을 선언합니다. 배열은 순서가 있기 때문입니다.
그리고 2중 for문을 사용합니다. 첫 번째 for문이 1번 돌 때 두 번째 for문은 5번 돌게됩니다.
그렇게 각각을 i와 j에 라는 변수의 이름으로 arr 배열 인덱스 0번째와 1번째에 넣어줍니다.
실행 결과
0 0 true
1 0 false
2 0 false
3 0 false
4 0 false
0 1 true
1 1 true
2 1 false
3 1 false
4 1 false
0 2 true
1 2 true
2 2 true
3 2 false
4 2 false
0 3 true
1 3 true
2 3 true
3 3 true
4 3 false
0 4 true
1 4 true
2 4 true
3 4 true
4 4 true
실행 결과 값이 작거나 같은 값이 인덱스 0번 즉 오름차순일 경우에는 true 입니다.
그와는 반대로 큰 값이 인덱스 0번 즉 내림차순일 경우에는 false 입니다.
“이제 두 값을 비교하여 오름차순으로 정렬되는 것을 확인하는 함수를 만들었으니 이번에는 실제 정렬을 해보도록하겠습니다.”
#include <iostream>
using namespace std;
bool CheckSorted(int a, int b) {
// TODO: ...
if (a <= b) {
return true;
} else {
return false;
}
}
// 정렬(sorting)
int main() {
int arr[2];
// TODO:
for (int j = 0; j < 5; j++) {
for (int i = 0; i < 5; i++) {
arr[0] = i;
arr[1] = j;
// swap 소개
if (arr[0] > arr[1]) {
swap(arr[0], arr[1]);
}
cout << boolalpha;
cout << arr[0] << " " << arr[1] << " "
<< (CheckSorted(arr[0], arr[1])) << endl;
}
}
return 0;
}
실행 결과
0 0 true
0 1 true
0 2 true
0 3 true
0 4 true
0 1 true
1 1 true
1 2 true
1 3 true
1 4 true
0 2 true
1 2 true
2 2 true
2 3 true
2 4 true
0 3 true
1 3 true
2 3 true
3 3 true
3 4 true
0 4 true
1 4 true
2 4 true
3 4 true
4 4 true
CPP에는 swap이라는 함수가 있습니다 swap을 사용할 경우에는 매개변수로 받은 두 값을 바꿔줍니다.
따라서 위와 같은 실행 결과를 출력합니다.
-
-
-
-
-
☕️[Java] 생성자 - 오버로딩 this()
생성자 - 오버로딩 this()
생성자도 메서드 오버로딩처럼 매개변수만 다르게 해서 여러 생성자를 제공할 수 있습니다.
MemberConstruct - 생성자 추가
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
기존 MemberConstruct에 생성자를 하나 추가해서 생성자가 2개가 되었습니다.
MemberConstruct(String name, int age)
MemberConstruct(String name, int age, int grade)
새로 추가한 생성자는 grade를 받지 않습니다. 대신에 grade는 50점이 됩니다.
package construct;
public class ConstructMain2 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
이름:user1 나이:15성적:90
이름:user2 나이:16성적:50
생성자를 오버로딩 한 덕분에 성적 입력이 꼭 필요한 경우에는 grade가 있는 생성자를 호출하면 되고, 그렇지 않은 경우에는 grade가 없는 생성자를 호출하면 됩니다.
grade가 없는 생성자를 호출하면 성적은 50점이 됩니다.
this()
두 생성자를 비교해 보면 코드가 중복되는 부분이 있습니다.
public MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
public MemberConstruct(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
바로 다음 부분입니다.
this.name = name;
this.age = age;
이때 this()라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있습니다.
참고로 this는 인스턴스 자신의 참조값을 가리킵니다. 그래서 자신의 생성자를 호출한다고 생각하면됩니다.
MemberConstruct - this() 사용
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this(name, age, 50);
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
이 코드는 첫번째 생성자 내부에서 두번째 생성자를 호출합니다.
MemberConstruct(String name, int age) -> MemberConstruct(String name, int age, int grade)
this()를 사용하면 생성자 내부에서 다른 생성자를 호출할 수 있습니다.
이 부분을 잘 활용하면 지금과 같이 중복을 제거할 수 있습니다.
물론 실행 결과는 기존과 같습니다.
this() 규칙
this()는 생성자 코드의 첫줄에만 작성할 수 있습니다.
다음은 규칙 위반입니다.
이 경우 컴파일 오류가 발생합니다.
public MemberConstruct(String name, int age) {
Sytem.out.println("go");
this(name, age, 50);
}
this()가 생성자 코드의 첫줄에 사용되지 않았습니다.
-
-
☕️[Java] this
this
Member - initMember() 추가
package construct;
public class Member {
String name;
int age;
int grade;
// 추가
void initMember( String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}
MethodInitMain3
package construct;
public class MethodInitMain3 {
public static void main(String[] args) {
Member member1 = new Member();
member1.initMember("user1", 15, 90);
Member member2 = new Member();
member2.initMember("user2", 16, 80);
Member[] members = { member1, member2 };
for (Member member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
initMember(...)는 Member에 초기값 설정 기능을 제공하는 메서드입니다.
다음과 같이 메서드를 호출하면 객체의 멤버 변수에 인자로 넘어온 값을 채웁니다.
member1.initMember("user1", 15, 90)
this
Member의 코드를 다시 봐봅시다.
initMember(String name...)의 코드를 보면 메서드의 매개변수에 정의한 String name과 Member의 멤버 변수의 이름이 String name으로 둘다 똑같습니다.
나머지 변수 이름도 name, age, grade로 모두 같습니다.
“멤버 변수와 메스더의 매개변수의 이름이 같으면 둘을 어떨게 구분해야 할까요?”
이 경우 멤버 변수보다 매개변수가 코드 블럭의 안쪽에 있기 때문에 “매개변수가 우선 순위를” 가집니다.
따라서 initMember(String name, ...) 메서드 안에서 name이라고 적으면 매개변수에 접근하게 됩니다.
멤버 변수에 접근하려면 앞에 this.이라고 해주면 됩니다.
여기서 this는 인스턴스 자신의 참조값을 가리킵니다.
진행 과정
this.name = name; // 1. 오른쪽의 name은 매개변수에 접근
this.name = "user"; // 2. name 매개변수의 값 사용
x001.name = "user"; // 3. this.은 인스턴스 자신의 참조값을 뜻함. 따라서 인스턴스의 멤버 변수에 접근.
this 제거
만약 이 예제에서 this를 제거하면 어떻게 될까요?
this.name = name
다음과 같이 수정하면 name은 둘다 매개변수를 뜻하게 됩니다.
따라서 멤버변수의 값이 변경되지 않습니다.
name = name;
정리
매개변수의 이름과 멤버 변수의 이름이 같은 경우 this를 사용해서 둘을 명확하게 구분해야 합니다.
this는 인스턴스 자신을 가리킵니다.
this의 생략
this는 생략할 수 있습니다.
이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수입니다.)를 먼저 찾고 없으면 그 다음으로 멤버 변수를 찾습니다.
멤버 변수도 없으면 오루가 발생합니다.
다음 예제는 필드 이름과 매개변수의 이름이 서로 다릅니다.
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
nameField = nameParameter;
}
}
“예를 들어서 nameField는 앞에 this가 없어도 멤버 변수에 접근합니다.”
nameField는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 없으므로 멤버 변수에서 찾습니다.
nameParametr는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 매개변수가 있으므로 매개변수를 사용합니다.
this와 코딩 스타일
“다음과 같이 멤버 변수에 접근하는 경우에 항상 this를 사용하는 코딩 스타일도 있습니다.”
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
this.nameField = nameParameter;
}
}
this.nameField를 보면 this를 생략할 수 있지만, 생략하지 않고 사용해도 됩니다.
이렇게 this를 사용하면 이 코드가 멤버 변수를 사용한다는 것을 눈으로 쉽게 확인할 수 있습니다.
그래서 과거에 이런 스타일을 많이 사용하기도 했습니다.
쉽게 이야기해서 this를 강제로 사용해서, 지역 변수(매개변수)와 멤버 변수를 눈에 보이도록 구분하는 것입니다.
“하지만 최근에는 IDE가 발전하면서 IDE가 멤버 변수와 지역 변수를 색상으로 구분해줍니다.”
이런 점 때문에 this는 꼭 필요한 경우에만 사용해도 충분하다 생각한다.
예를 들어 이런 경우 this.name = name -> name이 중복되는 것.
-
☕️[Java] 생성자 - 도입
생성자 - 도입
프로그래밍을 하다보면 객체를 생성하고 이후에 바로 초기값을 할당해야 하는 경우가 많습니다.
따라서 앞서 initMember(...)와 같은 메서드를 매번 만들어야 합니다.
“그래서 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 필요한 기능을 좀 더 편리하게 수행할 수 있도록 생성자라는 기능을 제공합니다.”
생성자를 사용하면 객체를 생성하는 시점에 즉시 필요한 기능을 수행할 수 있습니다.
생성자는 앞서 살펴본 initMember(...)와 같이 메서드와 유사하지만 몇가지 다른 특징이 있습니다.
아래 코드를 보면서 이해해봅시다.
MemberConstruct
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
“다음 부분이 바로 생성자입니다.”
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
“생성자는 메서드와 비슷하지만 다음과 같은 차이가 있습니다.”
생성자의 이름은 클래스 이름과 같아야 합니다. 따라서 첫 글자도 대문자로 시작합니다.
생성자는 반환 타입이 없습니다. 비워두워야 합니다.
나머지는 메서드와 같습니다.
ConstructMain1
package construct;
public class ConstructMain1 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16, 80);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
생성자 호출 name=user2,age=16,grade=80
이름:user1 나이:15성적:90
이름:user2 나이:16성적:80
생성자 호출
생성자는 인스턴스를 생성하고 나서 즉시 호출됩니다.
생성자를 호출하는 방법은 다음 코드와 같이 new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 됩니다.
new 생성자이름(생성자에 맞는 인수 목록)
new 클래스이름(생성자에 맞는 인수 목록)
참고로 생성자 이름이 클래스 이름이기 때문에 둘다 맞는 표현입니다.
new MemberConstruct("user1", 15, 90)
이렇게 하면 인스턴스를 생성하고 즉시 해당 생성자를 호출합니다.
여기서는 Memeber 인스턴스를 생성하고 바로 MemberConstruct(String name, int age, int grade) 생성자를 호출합니다.
참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호()도 포함해야 하는 이유가 바로 생성자 때문입니다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함합니다.
생성자 장점
중복 호출 제거
생성자가 없던 시절에는 생성 직후에 어떤 작업을 수행하기 위해 다음과 같이 메서드를 직접 한번 더 호출해야 했습니다.
“생성자 덕분에 객체를 생성하면서 동시에 생성 직후에 필요한 작업을 한번에 처리할 수 있게 되었습니다.”
// 생성자 등장 전
MemberInit member = new MemberInit();
member.initMember("user1", 15, 90);
// 생성자 등장 후
MemberConstruct member = new MemberConstruct("user1", 15, 90);
제약 - 생성자 호출 필수
위 코드에서 생성자 등장 전 코드를 보며 이해해봅시다.
“이 경우 initMember(...)를 실수로 호출하지 않으면 어떻게 될까요?”
이 메서드를 실수로 호출하지 않아도 프로그램은 작동합니다.
하지만 학생의 이름과 나이, 성적 데이터가 없는 상태로 프로그램이 작동하게 됩니다.
“만약에 이 값들을 필수로 반드시 입력해야 한다면, 시스템에 큰 문제가 발생할 수 있습니다.”
결국 아무 정보가 없는 유령 학생이 시스템 내부에 등장하게 됩니다.
생성자의 진짜 장점은 객체를 생성할 때 직접 정의한 생성자가 있다면 “직접 정의한 생성자를 반드시 호출” 해야 한다는 점입니다.
참고로 생성자를 메서드 오버로딩 처럼 여러개 정의할 수 있는데, 이 경우에는 하나만 호출하면 됩니다.
“MemberConstruct 클래스의 경우 다음 생성자를 직접 정의했기 때문에 직접 정의한 생성자를 반드시 호출해야 합니다.”
MemberConstruct(String name, int age, int grade) { ... }
다음과 같이 직접 정의한 생성자를 호출하지 않으면 컴파일 오류가 발생합니다.
MemberConstruct member3 = new MemberConstruct(); // 컴파일 오류 발생
member3.name = "user1";
컴파일 오류 메시지
java: constructor MemberConstruct in class construct.MemberConstruct cannot be applied to given types;
required: java.lang.String,int,int
found: no arguments
reason: actual and formal argument lists differ in length
컴파일 오류는 IDE에서 즉시 확인할 수 있는 좋은 오류입니다.
이 경우 개발자는 객체를 생성할 때, 직접 정의한 생성자를 필수로 호출해야 한다는 것을 바로 알 수 있습니다.
그래서 필요한 생성자를 찾아서 다음과 같이 호출할 것입니다.
MemberConstruct member = new MemberConstruct("user1", 15, 90);
“생성자 덕분에 학생의 이름, 나이, 성적은 항상 필수로 입력하게 됩니다.”
따라서 아무 정보가 없는 유령 회원이 시스템 내부에 등장할 가능성을 원천 차단합니다.
“생성자를 사용하면 필수값 입력을 보장할 수 있습니다.”
참고: 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램입니다.
-
☕️[Java] 생성자 - 필요한 이유
생성자 - 필요한 이유.
객체를 생성하는 시점에 어떤 작업을 하고 싶다면 “생성자(Construct)” 을 이용하면 됩니다.
생성자를 알아보기 전에 먼저 생성자가 왜 필요한지 코드로 간단히 알아봅시다.
MemberInit
package construct;
public class MemberInit {
String name;
int age;
int grade;
}
MethodInitMain1
package construct;
public class MethodInitMain1 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
member1.name = "user1";
member1.age = 15;
member1.grade = 90;
MemberInit member2 = new MemberInit();
member2.name = "user2";
member2.age = 16;
member2.grade = 80;
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
실행 결과
아름:user1 나이:15 성적:90
아름:user2 나이:16 성적:80
회원 객체를 생성하고 나면 name, age, grade 같은 변수에 초기값을 설정합니다.
아마도 회원 객체를 제대로 사용하기 위해서는 객체를 생성하자 마자 이런 초기값을 설정해야 할 것입니다.
이 코드에는 회원의 초기값을 설정하는 부분이 계속 반복됩니다.
메서드를 사용해서 반복을 제거해봅시다.
MethodInitMain2
package construct;
public class MethodInitMain2 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
initMember(member1, "user1", 15, 90);
MemberInit member2 = new MemberInit();
initMember(member2, "user2", 16, 80);
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
static void initMember(MemberInit member, String name, int age, int grade) {
member.name = name;
member.age = age;
member.grade = grade;
}
}
initMember(...) 메서드를 사용해서 반복을 제거했습니다.
그런데 이 메서드는 대부분 MemberInit 객체의 멤버 변수를 사용합니다.
우리는 앞서 객체 지향에 대해서 학습했습니다.
이런 경우 속성과 기능을 한 곳에 두는 것이 더 나은 방법입니다.
쉽게 이야기해서 MemberInit이 자기 자신의 데이터를 변경하는 기능(메서드)을 제공하는 것이 좋습니다.
-
-
-
-
-
☕️[Java] 객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍과 절차 지향 프로그래밍은 서로 대치되는 개념이 아닙니다.
객체 지향이라도 프로그램의 작동 순서는 중요합니다.
다만 어디에 더 초점을 맞추는가에 둘의 차이가 있습니다.
객체 지향의 경우 객체의 설계와 관계를 중시합니다.
반면 절차 지향의 경우 데이터와 기능이 분리되어 있고, 프로그램이 어떻게 작동하는지 그 순서에 초점을 맞춥니다.
절차 지향 프로그래밍
절차 지향 프로그래밍은 이름 그대로 절차를 지향합니다.
쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식입니다.
절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식입니다.
즉, “어떻게”를 중심으로 프로그래밍 합니다.
객체 지향 프로그래밍
객체 지향 프로그래밍은 이름 그대로 객체를 지향합니다.
쉽게 이야기해서 객체를 중요하게 생각하는 방식입니다.
객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객제들 간의 상호작용을 중심으로 프로그래밍하는 방식입니다.
즉 “무엇을” 중심으로 프로그래밍 합니다.
둘의 중요한 차이
절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있습니다.
반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 함께 포함되어 있습니다.
객체란?
세상의 모든 사물을 단순하게 추상화해보면 속성(데이터)과 기능(메서드) 딱 2가지로 설명할 수 있습니다.
자동차
속성: 색상, 속도
기능: 엑셀, 브레이크, 문 열기, 문 닫기
동물
속성: 색상, 키, 온도
기능: 먹는다, 걷는다
게임 캐릭터
속성: 레벨, 경험치, 소유한 아이템들
기능: 이동, 공격, 아이템 획득
“객체 지향 프로그래밍은 모든 사물을 속성과 기능을 가진 객체로 생각하는 것 입니다.”
객체에는 속성과 기능만 존재합니다.
이렇게 단순화하면 세상에 있는 객체들을 컴퓨터 프로그램으로 쉽게 설계할 수 있습니다.
이런 장점들 덕분에 지금은 객체 지향 프로그래밍이 가장 많이 사용됩니다.
참고로 실세계와 객체가 항상 1:1로 매칭되는 것은 아닙니다.
객체 지향의 특징은 속성과 기능을 하나로 묶는 것 뿐만 아니라 캡슐화, 상속, 다형성, 추상화, 메시지 전달 같은 다양한 특징들이 있습니다.
-
-
-
🍃[Spring] Gradle과 Maven
Gradle.
Gradle은 오픈 소스 빌드 자동화 시스템으로, 다양한 프로그래밍 언어와 프로젝트에 대해 유연한 빌드 스크립트를 제공합니다.
Groovy나 Kotlin DSL을 사용하여 빌드 스크립트를 작성하며, 이는 개발자가 읽기 쉽고, 강력하며, 사용자 정의가 가능한 빌드를 구성할 수 있게 합니다.
Gradle은 의존성 관리와 멀티 프로젝트 빌드를 지원하며, 이전에 실행된 작업의 출력을 캐시하여 빌드 시간을 단축시키는 증분 빌드 기능도 제공합니다.
Android 개발을 위한 공식 빌드 시스템으로도 널리 사용됩니다.
Maven.
Maven은 Java 프로젝트의 빌드, 문서화, 보고, 의존성 관리 등을 자동화하기 위한 또 다른 오픈 소스 빌드 도구입니다.
XML 형식의 pom.xml 파일을 사용하여 프로젝트 구성과 의존성을 관리합니다.
Maven은 중앙 저장소에서 필요한 라이브러리와 플러그인을 자동으로 다운로드하고, 프로젝트의 라이프사이클(컴파일, 테스트, 패키징 등)을 관리하는 표준화된 방법을 제공합니다.
이는 프로젝트의 일관성을 유지하고, 빌드 과정을 간소화하는 데 도움이 됩니다.
Gradle과 Maven의 차이점.
빌드 스크립트 구문 : Gradle은 Groovy나 Kotlin으로 작성된 빌드 스크립트를 사용하는 반면, Maven은 XML 기반의 pom.xml 파일을 사용합니다. Gradle의 DSL은 Maven의 XML보다 간결하고, 읽기 쉽습니다.
성능 : Gradle은 증분 빌드와 빌드 캐시 기능을 통해 Maven보다 빌드 시간을 단축시킬 수 있습니다. Maven은 Gradle에 비해 이러한 최적화 기능이 부족합니다.
유연성 : Gradle은 빌드 스크립트에 로직을 추가하여 빌드 프로세스를 매우 세밀하게 제어할 수 있습니다. Maven은 더 엄격한 라이프사이클과 구조를 따르며, 커스터마이징이 제한적입니다.
플러그인 생태계 : Maven은 오랜 기간 동안 사용되어 왔기 때문에 방대한 양의 플러그인이 있지만, Gradle도 활발히 성장하고 있는 플러그인 생태계를 갖추고 있습니다.
프로젝트 구조 : Maven은 규약을 중시하는 구조로, 프로젝트의 디렉토리 구조가 일정합니다. Gradle은 더 많은 구성 가능성을 제공하지만, 이는 동시에 프로젝트 설정이 복잡해질 수 있음을 의미합니다.
결론적으로, Gradler과 Maven은 각각의 장단점을 가지고 있으며, 프로젝트의 요구사항과 개발 팀의 선호도에 따라 적합한 도구를 선택하는 것이 중요합니다.
-
-
☕️[Java] 변수와 초기화
변수와 초기화
변수의 종류
멤버 변수(필드): 클래스에 선언.
지역 변수: 메서드에 선언, 매개변수도 지역 변수의 한 종류입니다.
멤버 변수, 필드 예시
public class Student {
String name;
int age;
int grade;
}
name, age, grade 는 멤버 변수입니다.
지역 변수 예시
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
Student student2 = new Student();
}
}
student1, student2는 지역 변수입니다.
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
a, x(매개변수, Parameter)는 지역변수입니다.
지역 변수는 이름 그대로 “특정 지역에서만 사용되는 변수라는 뜻입니다.”
예를 들어서 변수 x는 changePrimitive() 메서드의 블록에서만 사용됩니다.
changePrimitive() 메서드가 끝나면 제거됩니다.
a 변수도 마찬가지입니다. main() 메서드가 끝나면 제거됩니다.
변수의 값 초기화
멤버 변수: 자동 초기화.
인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화됩니다.
숫자(int) = 0, boolean = false, 참조형 = null(null 값은 참조할 대상이 없다는 뜻으로 사용됩니다.)
개발자가 초기값을 직접 지정할 수 있습니다.
지역 변수: 수동 초기화.
지역 변수는 항상 직접 초기화해야 합니다.
“멤버 변수의 초기화를 살펴봅시다”
InitData
package ref;
public class InitData {
int value1; // 초기화 하지 않음
int value2 = 10; // 10으로 초기화
}
value1은 초기값을 지정하지 않았고, value2는 초기값을 10으로 지정했습니다.
InitMain
package ref;
public class initMain {
public static void main(String[] args) {
InitData data = new InitData();
System.out.println("value1 = " + data.value1);
System.out.println("value2 = " + data.value2);
}
}
실행 결과
value1 = 0
value2 = 10
value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 됩니다.
숫자는 0으로 초기화됩니다.
value2는 10으로 초기값을 지정해두었기 때문에 객체를 생성할 때 10으로 초기화됩니다.
-
☕️[Java] 참조형과 메서드 호출 - 활용
참조형과 메서드 호출 - 활용
아래의 코드 class1.ClassStart3 코드에는 중복되는 부분이 2가지가 있습니다.
name, age, grade에 값을 할당하는 부분.
학생 정보를 출력하는 부분.
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
“이러한 중복은 메서드를 통해 손쉽게 제거할 수 있습니다.”
메서드에 객체 전달
다음과 같이 코드를 작성해봅시다.
Student
package ref;
public class Student {
String name;
int age;
int grade;
}
ref 패키지에도 Student 클래스를 만듭니다.
Method1
package ref;
public class Method1 {
public static void main(String[] args) {
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static void initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
참조형은 메서드를 호출할 때 참조값을 전달합니다.
따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있습니다.
initStudent(Student student, ...) : 전달한 학생 객체의 필드에 값을 설정합니다.
printStudent(Student student, ...) : 전달한 학생 객체의 필드 값을 읽어서 출력합니다.
initStudent() 메서드 호출 분석
initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
student1이 참조하는 Student 인스턴스에 값을 편리하게 할당하고 싶어서 initStudent() 메서드를 만들었습니다.
이 메서드를 호출하면서 student1을 전달합니다.
그러면 student1의 참조값(30f39991)이 매개변수 student에 전달됩니다.
이 참조값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 30f39991 Student 인스턴스에 접근하고 값을 변경할 수 있습니다.
참고: 30f39991은 실제 student1의 참조값입니다.
System.out.println("student1 참조값 : " + student1); 의 결과가 student1 참조값 : ref.Student@30f39991 이였습니다.
주의!
package ref;
import class1 Student;
public class Method1 {
...
}
import class1.Student;이 선언되어 있으면 안됩니다.
이렇게 되면 class1 패키지에서 선언한 Student를 사용하게 됩니다.
이 경우에는 접근 제어자 때문에 컴파일 오류가 발생합니다.
만약 선언되어 있다면 삭제해야 합니다. 삭제하면 같은 패키지에 있는 ref.Student를 사용합니다.
메서드에서 객체 반환
조금 더 코드를 리팩토링 시켜봅시다. 다음 코드에도 중복이 있습니다.
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
바로 객체를 생성하고, 초기값을 설정하는 부분입니다.
이렇게 2번 반복되는 부분을 하나로 합져봅시다.
“다음과 같이 기존 코드를 변경해봅시다.”
Method2
package ref;
public class Method2 {
public static void main(String[] args) {
Student student1 = createStudent("학생1", 15, 90);
Student student2 = createStudent("학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static Student createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
createStudent() 라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드안에 함께 포함했습니다.
“이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리합니다.”
그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 합니다.
그래야 메서드 밖에서 이 객체를 사용할 수 있습니다.
메서드는 호출 결과를 반환(return)을 할 수 있습니다.
메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 됩니다.
createStudent() 메서드 호출 분석
createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했습니다.
이 참조값만 있으면 해당 인스턴스에 접근할 수 있습니다.
여기서는 student1에 참조값을 보관하고 사용합니다.
진행 과정
Student student1 = createStudent("학생1", 15, 90); // 메서드 호출후 결과 반환
Student student1 = student(30f39991); // 참조형인 student를 반환
Student student1 = 30f39991; // student의 참조값 대입
student1 = 30f39991;
createStudent() 는 생성한 Student 인스턴스의 참조값을 반환합니다.
이렇게 반환된 참조값을 student1 변수에 저장합니다.
앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있습니다.
-
☕️[Java] 기본형과 참조형(3) - 메서드 호출
기본형과 참조형(3) - 메서드 호출
대원칙: 자바는 항상 변수의 값을 복사해서 대입합니다.
자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것입니다.
기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입합니다.
기본형이면 변수에 들어 있는 실제 사용하는 값을 복사해서 대입합니다.
참조형이면 변수에 들어 있는 참조값을 복사해서 대입합니다.
메서드 호출도 마찬가지입니다. 메서드를 호출할 때 사용하는 매개변수(parameter)도 결국 변수일 뿐 입니다.
따라서 메서드를 호출할 때 매개변수(parameter)에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달합니다.
다음 예시 코드를 봐봅시다.
기본형과 메서드 호출
package ref;
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
실행 결과
메서드 호출 전: a = 10
메서드 호출 후: a = 10
1. 메서드 호출
메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달합니다.
int x = a
이 코드는 다음과 같이 해석할 수 있습니다.
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다 따라서 변수 a,x 각각 숫자 10을 가지고 있습니다.
2. 메서드 안에서 값을 변경
int a = 10;
changePrimitive(a);
메서드 안에서 x = 20으로 새로운 값을 대입합니다.
결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지됩니다.
3. 메서드 종료
int a = 10;
changePrimitive(a);
메서드 종료후 값을 확인해보면 a는 10이 출력되는 것을 확인할 수 있습니다.
참고로 메서드가 종료되면 매개변수 x는 제거됩니다.
참조형과 메서드 호출
package ref;
public class MethodChange2 {
public static void main(String[] args) {
Data dataA = new Data();
dataA.value = 10;
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
changeReference(dataA);
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
}
static void changeReference(Data dataX) {
dataX.value = 20;
}
}
실행 결과
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
1. 메서드 호출
메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달합니다.
이 코드는 다음과 같이 해석할 수 있습니다.
int dataX = dataA;
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다.
변수 dataA는 참조값 x001을 가지고 있다는 가정하에 dataA는 참조값을 복사해서 전달합니다.
따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 됩니다.
이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있습니다.
2. 메서드 안에서 값을 변경
static void changeReference(Data dataX) {
dataX.value = 20;
}
메서드 안에서 dataX.value = 20으로 새로운 값을 대입합니다.
참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했습니다.
dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataX.value는 둘다 20이라는 값을 가집니다.
3. 메서드 종료
메서드 종료후 dataA.value의 값을 확인해보면 다음과 같이 20으로 변경된 것을 확인할 수 있습니다.
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
기본형과 참조형의 메서드 호출
자바에서 메서드의 매개변서(Parameter)는 항상 값에 의해 전달됩니다.
그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라집니다.
기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)의 값을 변경해도, 호출자의 변수 값에는 영향이 없습니다.
참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경됩니다.
-
-
-
☕️[Java] 기본형과 참조형(1)
기본형과 참조형(1)
자바에서 참조형을 제대호 이해하는 것은 정말 중요합니다.
변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있습니다.
사용하는 값을 변수에 직접 넣을 수 있는 기본형
이전 포스팅의 예시 코드에서 본 Student student1 과 같이 객체가 저장된 메모리의 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있습니다.
기본형(Primitive Type)
Int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 “기본형” 이라고 합니다.
참조형(Reference Type)
Student student1, int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 “참조형” 이라고 합니다.
참조형은 객체 또는 배열에 사용됩니다.
쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)이 들어가 있습니다.
참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 위치로 이동해야 합니다.
기본형 vs 참조형 - 기본
“기본형” 은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있습니다.
때문에 해당 값을 바로 사용할 수 있습니다.
“참조형” 은 실제 사용하는 값을 변수에 담는 것이 아닙니다.
이름 그대로 실제 객체의 위치(참조, 주소)를 저장합니다.
참조형에는 객체와 배열이 있습니다.
“객체” 는 .(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있습니다.
“배열” 은 []를 통해서 메모리 상에 생성된 배열을 찾아야가 사용할 수 있습니다.
기본형 vs 참조형 - 계산
기본형은 들어있는 값을 그대로 계산에 사용할 수 있습니다.
예) 더하고 빼고, 사용하고 등등,(숫자 같은 것들은 바로 계산할 수 있습니다.)
참조형은 들어있는 참조값을 그대로 사용할 수 없습니다.
주소지만 가지고는 할 수 있는게 없습니다.
주소지에 가야 실체가 있기 때문입니다.
예) 더하고 빼고 사용하고를 못합니다. 참조값만 가지고는 계산할 수 있는 것이 없습니다.
“기본형은 연산이 가능하지만 참조형은 연산이 불가능합니다.”
int a = 10, b = 20;
int sum = a + b;
기본형은 변수에 실제 사용하는 값이 담겨있습니다.
따라서 +, -와 같은 연산이 가능합니다.
Student s1 = new Student();
Student s2 = new Student();
s1 + s2 // 오류 발생.
참초형은 변수에 객체의 위치인 참조값이 들어있습니다.
참조값은 계산에 사용할 수 없습니다. 따라서 오류가 발생합니다.
“물론 다음과 같이 .(dot)을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있습니다.”
Student s1 = new Student();
s1.grade = 100;
Student s2 = new Student();
s2 grade 90;
int sum = s1.grade + s2.grade // 연산 가능
쉽게 이해하는 팁
“기본형을 제외한 나머지는 모두 참조형입니다.”
기본형은 소문자로 시작합니다.
int, long, double, boolean 모두 소문자로 시작합니다.
기본형은 자바가 기본으로 제공하는 데이터 타입입니다.
이러한 기본형은 개발자가 새로 정의할 수 없습니다.
개발자는 참조형인 클래스만 직접 정의할 수 있습니다.
클래스는 대문자로 시작합니다.
Student, String, 등…
클래스는 모두 참조형입니다.
“참고 - String”
자바에서 String은 특별합니다.
String은 사실 클래스입니다. 즉, 참조형입니다.
그런데 기본형처럼 문자 값을 바로 대입할 수 있습니다.
문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공합니다.
String에 대한 자세한 내용은 추후에 설명하겠습니다.
-
-
-
-
-
-
🐋[MySQL] 테이블에 데이터 입력 INSERT INTO
INSERT INTO
memberTBL이라는 테이블이 있습니다.
그 테이블에는 아무런 데이터가 입력되지 않은 상태입니다.
열(Column, 필드)은 총 3개입니다.
memberID
char(8)
memberName
char(5)
memberAddress
char(20)
데이터를 한 행(row)을 삽입하려고 할 경우에는 다음과 같이하면 됩니다.
INSERT INTO 테이블이름 VALUES (데이터1, 데이터2, 데이터3...);
만약 memberTBL에 한 행(row)을 삽입하려 할 경우는 다음과 같이하면 됩니다.
INSERT INTO memberTBL VALUES ('Thomas', '토마스', '경기 부천시 중동');
만약 특정 필드에만 값을 입력하고 싶을 경우에는 VALUES 앞에 “입력할 필드를 선언해 줍니다.”
INSERT INTO memberTBL (memberID, memberAddress) VALUES ('Thomas', '경기 부천시 중동');
만약 여러 개의 행(row)을 동시에 입력하고 싶을 경우에는 아래와 같이 여러 개의 행을 동시에 입력이 가능합니다.
INSERT INTO memberTBL (memberID, memberName, memberAddress) VALUES ('Thomas', '토마스', '경기 부천시 중동'), ('Edward', '에드워드', '서울 은평구 증산동'), ('Henrv', '헨리', '인천 남구 주안동'), ('Gorden', '고든', '경기 성남시 분당구');
-
-
-
☕️[Java] 배열 도입
배열 도입
“배열” 을 사용하면 특정 타입을 연속한 데이터 구조로 묶어서 편리하게 관리할 수 있습니다.
Student 클래스를 사용한 변수들도 Student 타입이기 때문에 학생도 “배열” 을 사용해서 “하나의 데이터 구조로 묶어서 관리” 할 수 있습니다.
클래스의 도입 포스팅 예시 코드 참고 -> 학생 클래스.
Student 타입을 사용하는 “배열” 을 도입해봅시다.
package class1;
public class ClassStart4 {
public static void main(String[] args) {
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student[] students = new Student[2];
students[0] = student1;
students[1] = student2;
for (int i = 0; i < students.length; i++) {
System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade);
}
}
}
코드를 분석해 봅시다.
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student 클래스를 기반으로 student1, student2 인스턴스를 생성합니다. 그리고 필요한 값을 채워둡니다.
배열에 참조값 대입
이번에는 Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관해봅시다.
Student[] students = new Student[2];
Student 변수를 2개 보관할 수 있는 사이즈 2의 배열을 만듭니다.
Student 타입의 변수는 Student 인스턴스의 “참조값을 보관” 합니다.
Student 배열의 각각의 항목도 “Student 타입의 변수일 뿐” 입니다. 따라서 “Student 타입의 참조값을 보관” 합니다.
student1, student2 변수를 생각해보면 Student 타입의 참조값을 보관합니다.
배열에는 아직 참조값을 대입하지 않았기 때문에 참조값이 없다는 의미의 null 값으로 초기화 됩니다.
이제 배열에 객체를 보관해보겠습니다.
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
student1, student2에는 참조값이 보관되어 있습니다.
따라서 이 참조값이 배열에 저정됩니다.
또는 student1, student2에 보관된 참조값을 읽고 복사해서 배열에 대입한다고 표현합니다.
이제 배열은 x001, x002의 참조값을 가집니다.
참조값을 가지고 있기 때문에 x001(학생1), x002(학생), Student 인스턴스에 모두 접근할 수 있습니다.
너무 중요해서 한 번더 강조합니다 잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
자바에서 변수의 대입(=)은 모두 변수에 들어있는 값을 복사해서 전달하는 것입니다.
이 경우 오른쪽 변수인 student1, student2에는 참조값이 들어있습니다.
그래서 이 값을 복사해서 왼쪽에 있는 배열에 전달합니다.
따라서 기존 student1, student2에 들어있던 참조값은 당연히 그대로 유지됩니다.
주의!!
변수에는 인스턴스 자체가 들어있는 것이 아닙니다!
“인스턴스의 위치를 가리키는 참조값이 들어있을 뿐입니다!!”
따라서 대입(=)시에 인스턴스가 복사되는 것이 아니라 참조값만 복사됩니다.
배열에 들어있는 객체 사용
배열에 들어있는 객체를 사용하려면 먼저 배열에 접근하고, 그 다음에 객체에 접근하면 됩니다.
이전에 설명한 그림과 코드를 함께 보면 쉽게 이해가 될 것입니다.
학생1 예제
System.out.println(students[0].name); // 배열 접근 시작
System.out.println(x005[0].name); // [0]를 사용해서 x005 배열의 0번 요소에 접근
System.out.println(x001.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생1");
학생2 예제
System.out.println(students[1].name); // 배열 접근 시작
System.out.println(x005[1].name); // [0]를 사용해서 x005 배열의 1번 요소에 접근
System.out.println(x002.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생2");
-
-
-
☕️[Java] 클래스, 객체, 인스턴스 정리
클래스, 객체, 인스턴스 정리.
클래스 - Class
클래스는 객체를 생성하기 위한 “틀” 또는 “설계도” 입니다.
클래스는 객체가 가져야 할 “속성(변수)” 와 “기능(메서드)” 를 정의합니다.
예를 들어 학생이라는 클래스는 속성(변수)으로 name, age, grade를 가집니다.
참고로 기능(메서드)은 추후에 설명합니다. 지금은 속성(변수)에 집중합시다.
“틀” : 붕어빵 틀을 생각해봅시다.
붕어빵 틀은 붕어빵이 아닙니다! 이렇게 생긴 붕어빵이 나왔으면 좋겠다고 만드는 틀일 뿐입니다.
실제 먹을 수 있는 것이 아닙니다. 실제 먹을 수 있는 팥 붕어빵을 객체 또는 인스턴스라고 합니다.
“설계도” : 자동차 설계도를 생각해봅시다.
자동차 설계도는 자동차가 아닙니다! 설계도는 실제 존재하는 것이 아니라 개념으로만 있는 것입니다.
설계도를 통한 생산한 실제 존재하는 흰색 테슬라 모델 Y 자동차를 객체 또는 인스턴스라고 합니다.
객체 - Object
객체는 클래스에서 정의한 속성과 기능을 가진 실체입니다.
객체는 서로 독립적인 상태를 가집니다.
예를 들어 위 코드에서 student1은 학생1의 속성을 가지는 객체이고, student2는 학생2의 속성을 가지는 객체입니다.
student1 과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체입니다.
인스턴스 - Instance
인스턴스는 특정 클래스로부터 생성된 객체를 의미합니다.
그래서 객체와 인스턴스라는 용어는 자주 혼용됩니다.
인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용합니다.
예를 들어서 “student1 객체는 Student 클래스의 인스턴스다.” 라고 표현합니다.
객체 vs 인스턴스
둘 다 클래스에서 나온 실체라는 의미에서 비슷하게 사용되지만, 용어상 인스턴스는 “객체보다 좀 더 관계에 초점을 맞춘 단어입니다.”
보통 “student1은 Student의 객체이다.” 라고 말하는 대신 “student1은 Student의 인스턴스이다.” 라고 특정 클래스와의 “관계를 명확히 할 때 인스턴스” 라는 용어를 주로 사용합니다.
좀 더 쉽게 풀어보자면, 모든 인스턴스는 객체이지만, 우리가 “인스턴스” 라고 부르는 순간 “특정 클래스로부터 그 객체가 생성되었음을 강조하고 싶을 때입니다.”
예를 들어, student1은 객체이지만, 이 객체가 Student 클래스로부터 생성된다는 점을 명확히 하기 위해 student1을 Student의 인스턴스라고 부릅니다.
하지만 둘 다 클래스에서 나온 실체라는 핵심 의미는 같기 때문에 보통 둘을 구분하지 않고 사용합니다.
-
☕️[Java] 클래스 도입
클래스 도입
클래스를 사용해서 “학생”이라는 개념을 만들고, 각 학생 별로 본인의 이름, 나이, 성적을 관리해봅시다.
우선 코드를 봐봅시다.
package class1;
public class Student {
String name;
int age;
int grade;
}
class 키워드를 사용해서 학생 클래스(Student)를 정의합니다.
학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가집니다.
이렇게 “클래스에 정의한 변수들을 멤버 변수, 또는 필드” 라 합니다.
멤버 변수(Member Variable) : 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부릅니다.
필드(Field) : 데이터 항목을 가르키는 전통적인 용어입니다. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라고 합니다.
자바에서 멤버 변수, 필드는 같은 뜻입니다.
클래스에 소속된 변수를 뜻합니다.
클래스는 관례상 대문자로 시작하고 낙타 표기법을 사용합니다.
예) Student, User, MemberService
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
실행 결과
이름:학생1 나이:15 성적:90
이름:학생2 나이:16 성적:80
클래스와 사용자 정의 타입
“타입” 은 데이터의 종류나 형태를 나타냅니다.
int라고 하면 정수 타입, String이라고 하면 문자 타입입니다.
그러면 학생(Student)이라는 타입을 만들면 되지 않을까요?
클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있습니다.
사용자가 직접 정의하는 “사용자 정의 타입” 을 만들려면 설계도가 필요합니다.
이 “이 설계도가 바로 클래스” 입니다.
“설계도”인 “클래스”를 사용해서 “실제 메모리에 만들어진 실체를 객체 또는 인스턴스” 라 합니다.
“클래스”를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할 수 있습니다.
용어: 클래스, 객체, 인스턴스
클래스는 설계도이고, 이 설계도를 기반으로 실제 메모리에 만들어진 실체를 “객체 또는 인스턴스” 라 합니다.
둘다 같은 의미로 사용됩니다.
여기서는 학생(Student) 클래스를 기반으로 학생1(student1), 학생2(student2) 객체 또는 인스턴스를 만들었습니다.
코드 분석
1. 변수 선언.
Student student1 // Student 변수 선언
Student student1
Student “타입” 을 받을 수 있는 변수를 선언합니다.
int는 정수를, String은 문자를 담을 수 있듯이 “Student는 Student 타입의 객체(인스턴스)를 받을 수 있습니다.”
2. 객체 생셩.
student1 = new Student() // Student 인스턴스 생성
student1 = new Student() 코드를 나누어 분석해봅시다.
객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 합니다.
new Student()에서의 new는 새로 생성한다는 뜻 입니다.
new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻입니다.
이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성합니다.
객체를 생성할 때는 new 클래스명()을 사용하면 됩니다. 마지막에 ()도 추가해야합니다.
Student 클래스는 String name, int age, int grade 멤버 변수를 가지고 있습니다.
이 변수를 사용하는데 필요한 메모리 공간도 함께 확보합니다.
3. 참조값 보관
student1 = x001l // Student 인스턴스 참조값 보관
객체를 생성시 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)(x001)을 반환합니다.
여기셔 x001이라고 표현한 것이 참조값입니다.(실제로 x001처럼 표현되는 것은 아니고 이해를 돕기 위한 예시입니다.)
new 키워드를 통해 객체가 생성되고 나면 참조값을 반환합니다.
앞서 선언한 변수인 Student student1에 생성된 객체의 참조값(x001)을 보관합니다.
Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있습니다.
student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있습니다.
따라서 이 변수를 통해서 객체를 접근(참조)할 수 있습니다. 쉽게 이야기해서 student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있습니다.
참조값을 변수에 보관해야 하는 이유
객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없습니다.
“이 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 만드는 것 입니다.”
따라서 생성한 객체에 접근할 수 있는 방법이 필요합니다.
이런 이유로 객체를 생성할 때 반환되는 참조값을 어딘가에 보관해두어야 합니다.
앞서 Student student1 변수에 참조값(x001)을 저장해두었으므로 저장한 참조값(x001)을 통해서 실제 메모리에 존재하는 객체에 접근할 수 있습니다.
지금까지 설명한 내용을 간단히 풀어보면 다음과 같습니다.
Student student1 = new Student(); // 1. Student 객체 생성
Student student1 = x001; // 2. new Student()의 결과로 x001 참조값 변환
student1 = x001; // 3. 최종 결과
이후에 학생2(student2)까지 생성하면 다음과 같이 Student 객체(인스턴스)가 메모리에 2개 생성됩니다.
“각각의 참조값이 다르므로 서로 구분할 수 있습니다.”
참조값을 확인하고 싶다면 다음과 같이 객체를 담고 있는 변수를 출력해보면 됩니다.
System.out.println(student1);
System.out.println(student2);
출력 결과
class1.Student@30f39991
class1.Student@452b3a41
@ 앞은 패키지 + 클래스 정보를 뜻합니다. @ 뒤에 16진수는 참조값을 뜻합니다.
-
💾[Database] SQL의 개요
SQL의 개요
SQL은 관계형 DB에서 사용되는 언어로 ‘에스큐엘’ 또는 ‘시퀄’이라고 읽습니다.
관계형 DBMS(그중에서도 MySQL)를 배우려면 SQL을 익히는 것은 필수입니다.
SQL은 DB를 조작하는 ‘언어’로, 일반적인 프로그래밍 언어(C, C++, Java, C# 등)와 다른 특성을 가지고 있습니다.
SQL은 국제 표준화기관에서 표준화된 내용을 계속 발표했습니다.
SQL의 특징
“DBMS 제작 회사와 독립적이다.”
모든 DBMS 제작 회사에서 표준 SQL이 공개되어 각 회사는 이 표준 SQL에 맞춰 DBMS를 개발합니다.
따라서 SQL은 대부분의 DBMS 제품에서 공통적으로 호환됩니다.
“다른 시스템으로의 이식성이 좋다.”
SQL은 서버용, 개인용, 휴대용 장비 등 운영되는 DBMS마다 상호 호환성이 뛰어납니다.
한 시스템에서 사용하던 SQL을 다른 시스템으로 이식하는 데 큰 문제가 없습니다.
“표준이 계속 발전합니다.”
SQL은 SQL-86, 89, 92, 1999, 2003, 2008, 2011 등으로 개선된 표준안이 계속 발표되었으며, 지금도 개선된 안이 꾸준히 연구되고 있습니다.
“대화식 언어입니다.”
기존 프로그래밍 언어는 프로그램 작성, 컴파일 및 디버깅, 실행 과정을 거쳐야만 그 결과를 확인할 수 있지만 SQL은 바로 질의하고 결과를 얻는 대화식 언어입니다.
“클라이언트/서버 구조를 지원합니다.”
SQL은 분산형 구조인 클라이언트/서버 구조를 지원합니다.
클라이언트에서 질의를 하면 서버에서 그 질의를 받아 처리하여 클라이언트에 전달하는 구조입니다.
SQL을 사용시 주의할 점은, 모든 DBMS 제품의 SQL 문이 완벽하게 동일하지는 않다는 것입니다.
많은 회사가 되도록 표준 SQL을 준수하려고 노력하지만 각 회사의 DBMS마다 특징이 있기 때문에 현실적으로 완전히 통일되기는 어렵습니다.
각 회사는 가급적 표준 SQL을 지키면서도 자신의 제품에 특화된 SQL을 사용합니다.
이를 오라클에서는 PL/SQL, SQL Server에서는 T-SQL이라 부르고 MySQL에서는 그냥 SQL이라 일컫습니다.
아래 그림과 같이 각회사의 제품은 모두 표준 SQL을 공통으로 사용하면서 자기 제품의 특성에 맞춘 호환되지 않는 SQL 문도 사용합니다.
-
☁️[AWS] Route 53에 등록된 서브도메인 github page에 연결하기
AWS Route 53에 등록된 서브도메인 github page에 연결.
고통의 시작.
블로그를 처음 만들고 잘 운영하던 중 jekyll과 Gem Dependency와 Ruby 버전 그리고 Bundle까지 뭔가 엉키고 꼬여서 풀리지 않고 어느새 블로그는 엉망이 되어버렸습니다.
진심으로 복구 시도를 열심해 했으나 내 부족한 지식으로 인하여 밀어버릴 수 밖에…
그리하여 깔끔하게 데이터 백업 후 밀어버리고 더 이쁜 블로그 테마를 찾아버렸습니다(오히려 좋아👍)
그렇게 아주 기분 좋게 블로그를 옮기고 잘 운영되나 싶었는데 ‘얼씨구?’ AWS Route 53에서 등록한 서브도메인을 github page와 연결해 놓았었는데 이 친구가 아주 먹통이 되어버렸습니다.
“DNS가 깃헙 서버와 맞지 않는다는 경고를 내보내주었습니다.”
이상하게 찝찝하게도 그런데 접속은 정상적으로 동작했습니다…
아….. 찝찝한게 제일 싫은 나는 결국 오전 4시 기상해서 하나씩 알아보기 시작했습니다.
DNS
Domain Name System(이후 DNS)은 인터넷의 전화번호부 같은 역할을 합니다.
사람들이 웹사이트에 접속시, 일반적으로 기억하기 쉬운 도메인 이름(예: www.devkobe24.com)을 사용합니다.
그러나 인터넷 자체는 IP 주소(예:192.0.2.1)라는 숫자 주소 체계를 사용하여 컴퓨터들 사이의 통신을 가능하게 합니다.
“DNS는 사용자가 웹 브라우저에 입력한 도메인 이름을 컴퓨터가 이해할 수 있는 IP 주소로 변환하는 시스템입니다.”
이를 통해 사용자는 복잡한 IP 주소를 기억하지 않고도 웹사이트에 쉽게 접근할 수 있습니다.
DNS의 주요 기능과 구성요소
도메인 이름 해석 : 사용자가 웹 브라우저에 URL을 입력하면, DNS 서버는 해당 URL의 도메인 이름을 IP 주소로 변환합니다. 이 과정을 “이름 해석” 또는 “도메인 이름 해석”이라고 합니다.
계층적 구조 : DNS 시스템은 계층적 구조로 되어 있습니다. 맨 위에는 루트 DNS 서버가 있으며, 그 아래에는 최상위 도메인(TLD)서버(예: .com, .net, .org등), 그리고 더 아래에는 권한 있는 이름 서버가 위치합니다. 권한 있는 이름 서버는 특정 도메인(예: devkobe24.com)에 대한 정보를 관리합니다.
캐싱 : DNS 쿼리의 효율성을 높이기 위해, DNS 서버는 해석된 주소 정보를 일정 시간 동안 저장(캐싱)합니다. 이렇게 하면 같은 요청에 대해 반복적으로 최상위 서버에 접근할 칠요가 없어집니다.
재귀적 및 반복 쿼리 : 사용자의 DNS 쿼리는 먼저 로컬 DNS 서버로 전송됩니다. 로컬 DNS 서버는 요청된 도메인의 IP 주소를 알고 있으면 바로 응답합니다. 모르는 경우, 다른 DNS 서버에 요청을 전달하여 답을 찾습니다. 이 과정에서 재귀적 쿼리(사용자를 대신해 답을 찾는 과정)와 반복적 쿼리(요청을 다른 서버로 전달하는 과정)가 사용됩니다.
DNS는 인터넷 사용의 핵심요소로, 사용자가 웹사이트에 접근하고, 이메일을 보내고 받으며, 다양한 온라인 서비스를 이용할 수 있게 해줍니다.
이렇게 DNS에 대하여 알아보고나서 이전에 연결을 어떻게 했었는지 이전에 블로그를 찾아봤습니다.
먼저 연결 전 사전 준비물(?)이 필요합니다.
도메인
Route 53 DNS 호스팅 설정
Github Pages 설정된 Repository
이렇게 준비 한 뒤 Route 53 콘솔을 세팅합니다.
Route 53 콘솔 설정.
Route 53 콘솔로 갑니다 > 호스팅 영역을 클릭 > 보유 도메인을 설정합니다.
레코드 생성
레코드 이름 : 서브(2차) 도메인 명 > www
레코드 유형 : CNAME
값 : (자신의 깃헙 레포 이름) > ex) devKobe24.github.io
Github Repository 설정.
레포지토리 > 셋팅 > 페이지 > 커스텀 도메인
그 안에 Route 53에서 내가 등록한 커스텀 도메인 명을 입력합니다. (예 : www.devkobe24.com)
위와 같이 등록해주고 초록색 글자로 “DNS check successful”이 나오면 성공입니다.
하지만 저는 나오지 않았습니다.
그래서!! 더 알아 본 결과!!
A레코드 추가.
Route 53에서 A레코드를 추가해주었어야 했습니다.
Route 53 > 호스팅 영역 > 등록할 호스팅 영역의 이름 클릭 > 레코드 생성
레코드 이름은 비워둡니다.
레코드 유형은 A
값은 다음 IP 중 하나를 골라 사용합니다.
185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
나머지 옵션은 건들지 않습니다.
이후 레코드 생성을 눌러 생성합니다.
이후 깃헙 레포로 돌아갑니다.
위 그림과 같이 다시 도메인을 넣고 체크해보면 성공적으로 도메인이 적용됩니다, 저는 이렇게 도메인을 성공적으로 적용했습니다!!
참고자료
AWS Route 53에 등록된 서브도메인을 GitHub Pages에 연결하기
-
-
-
☕️[Java] 클래스가 필요한 이유.
클래스가 필요한 이유.
먼저 아래의 학생 정보 출력 프로그램을 만들어 봅시다.
이 프로그램은 두 명의 학생 정보를 출력하는 프로그램입니다.
각 학생은 이름, 나이, 성적을 가지고 있습니다.
요구사항
첫 번째 학생의 이름은 “학생1”, 나이는 15, 성적은 90 입니다.
두 번째 학생의 이름은 “학생2”, 나이는 16, 성적은 80 입니다.
각 학생의 정보를 다음과 같은 형식으로 출력해야 합니다: "이름: [이름] 나이: [나이] 성적: [성적]"
변수를 사용해서 학생 정보를 저장하고 변수를 사용해서 학생 정보를 출력해야 합니다.
예시 출력
이름: 학생1 나이: 15 성적: 90
이름: 학생2 나이: 16 성적: 80
변수를 사용해서 이 문제를 풀어보면 다음과 같이 프로그램을 만들어볼 수 있습니다.
package class1;
public class ClassStart1 {
public static void main(String[] args) {
String firstStudentName = "학생1";
String secondStudentName = "학생2";
int firstStudentAge = 15;
int secondStudentAge = 16;
int firstStudentGrade = 90;
int secondStudentGrade = 80;
System.out.println("이름: " + firstStudentName + " 나이: " + firstStudentAge + " 성적: " + firstStudentGrade);
System.out.println("이름: " + secondStudentName + " 나이: " + secondStudentAge + " 성적: " + secondStudentGrade);
}
}
위 코드에서는 학생 2명을 다루어야 하기 때문에 각각 다른 변수를 사용했습니다.
이 코드의 문제는 학생이 늘어날 때 마다 변수를 추가로 선언해야 하고, 또 출력하는 코드도 추가해야 합니다.
이런 문제를 어떻게 해결할 수 있을까요?
이번에는 위 코드를 “배열을 사용하여 리펙토링” 해봅시다.
아래의 코드는 “배열을 활용한 코드” 입니다.
package class1;
public class ClassStart2 {
public static void main(String[] args) {
String[] studentNames = { "학생1", "학생2" };
int [] studentAges = { 15, 16 };
int [] studentGrades = { 90, 80 };
for (int i = 0; i < studentNames.length; i++) {
System.out.println("이름: " + studentNames[i] + " 나이: " + studentAges[i] + " 성적: " + studentGrades[i]);
}
}
}
이전 코드보다 훨씬 깔끔해졌으며, 위 코드에서 문제가 됐었던 학생이 늘어날 때 마다 변수를 새롭게 추가할 필요도 없어졌습니다.
배열 사용의 한계.
하지만 배열을 사용해서 코드를 최소화하는데 성공했지만 “한 한생의 데이터가 studentNames[], studentAges[], studentGrades라는 3개의 배열에 나누어져 있습니다.”
따라서 “데이터를 변경할 때 매우 조심해서 작업해야 합니다.”
예를 들어서 학생 2의 데이터를 제거하려면 각각의 배열마다 학생2의 요소를 정확하게 찾아서 제거해주어야 합니다.
학생2 제거
String[] studentNames = { "학생1", "학생3", "학생4", "학생5" };
int [] studentAges = { 15, 17, 10, 16 };
int [] studentGrades = { 90, 100, 80, 50 };
한 학생의 데이터가 3개의 배열에 나누어져 있기 때문에 3개의 배열을 각각 변경해야 합니다!!
그리고 한 학생의 데이터를 관리하기 위해 3개의 배열의 인덱스 순서를 항상 정확하게 맞추어야 합니다.(조금이라도 실수하면 😱)
이렇게 하면 특정 학생의 데이터를 변경시 실수할 가능성이 매우 높습니다.
이 코드는 컴퓨터가 볼 때는 아무 문제가 없지만, 사람이 관리하기에는 좋은 코드가 아닙니다. 😵💫
정리
지금처럼 이름, 나이, 성적을 각각 따로 나누어서 관리하는 것은 사람이 관리하기 좋은 방식이 아닙니다.
사람이 관리하기 좋은 방식은 학생이라는 개념을 하나로 묶는 것입니다.
그리고 각각의 학생 별로 본인의 이름, 나이, 성적을 관리하는 것 입니다.
-
☕️[Java] 메서드 파트 정리.
정리
변수명 vs 메서드명
변수 이름은 일반적으로 명사를 사용합니다.
한편 메서드는 무언가 동작하는데 사용하기 때문에 일반적으로 동사로 시작합니다.
이런 차이점 외에는 변수 이름과 메서드 이름에 대한 규칙은 둘다 같습니다.
변수명 예): customerName, totalSum, employeeCount, isAvailable
메서드명 예): printReport(), calculateSum(), addCustomer(), getEmployeeCount(), setEmployeeName()
메서드 사용의 장점
코드 재사용 : 메서드는 특정 기능을 캡슐화하므로, 필요할 때마다 그 기능을 다시 작성할 필요 없이 해당 메서드를 호출함으로써 코드를 재사용할 수 있습니다.
코드의 가독성 : 이름이 부여된 메서드는 코드가 수행하는 작업을 명확하게 나타내므로, 코드를 읽는 사람에게 추가적인 문맥을 제공합니다.
모듈성 : 큰 프로그램을 작은, 관리 가능한 부분으로 나눌 수 있습니다. 이는 코드의 가독성을 향상시키고 디버깅을 쉽게 만듭니다.
코드 유지 관리 : 메서드를 사용하면, 코드의 특정 부분에서 문제가 발생하거나 업데이트가 필요한 경우 해당 메서드만 수정하면 됩니다. 이렇게 하면 전체 코드 베이스에 영향을 주지 않고 변경 사항을 적용할 수 있습니다.
재사용성과 확장성 : 잘 설계된 메서드는 다른 프로그램이나 프로젝트에서도 재사용할 수 있으며, 새로운 기능을 추가하거나 기존 기능을 확장하는 데 유용합니다.
추상화 : 메서드를 사용하는 곳에서는 메서드의 구현을 몰라도 됩니다. 프로그램의 다른 부분에서는 복잡한 내부 작업에 대해 알 필요 없이 메서드를 사용할 수 있습니다.
테스트와 디버깅 용이성 : 개별 메서드는 독립적으로 테스트하고 디버그할 수 있습니다. 이는 코드의 문제를 신속하게 찾고 수정하는데 도움이 됩니다.
따라서, 메서드는 효율적이고 유지 보수가 가능한 코드를 작성하는 데 매우 중요한 도구입니다.
자바에서의 대원칙.
“자바는 항상 변수의 값을 복사해서 대입합니다.”
이 대원칙은 반드시 이해해야 합니다. 그러면 아무리 복잡한 상황에서도 코드를 단순하게 이해할 수 있습니다.
package method;
public class MethodValue0 {
public static void main(String[] args) {
int num1 = 5;
int num2 = num1;
num2 = 10;
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
}
}
실행 결과
num1 = 5
num2 = 10
용어: 메서드 시그니처(method signature)
메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)
메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻합니다.
메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있습니다.
쉽게 이야기해서 메서드를 구분할 수 있는 기준입니다.
자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야 합니다. 그래야 어떤 메서드를 호출 할 지 결정할 . 수있습니다.
따라서 메서드 오버로딩과 같이 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주합니다.
반환 타입은 시그니처에 포함되지 않습니다.
-
-
💾[Database] DBMS의 분류
DBMS의 분류.
“DBMS” 는 크게 계층형(hierarchical), 망형(network), 관계형(relational), 객체지향형(object-oriented), 객체관계형(object-relational) 으로 분류됩니다.
“계층형(Hierachical) DBMS”
1960년대에 처음 등장한 DBMS 개념입니다.
아래 그림에서 보듯이 각 계층이 트리 형태를 띠고 1:N 관계를 갖습니다.
예를 들어 사장 1명에 부서 3개가 연결되어 있는 구조가 계층형 구조입니다.
계층형 DBMS는 구축한 후 구조를 변경하기가 상당히 까다롭습니다.
주어진 상태에서 검색은 빠르나 접근의 유연성이 부족하여 임의 검색 시 어려움이 있는 것이 단점입니다.
“망형(network) DBMS”
계층형(Hierachical) DBMS의 문제점을 개선하기 위해 1970년대에 시작되었습니다.
1:1, 1:N, N:M(다대다) 관계가 지원되어 효과적이고 빠른 데이터 추출이 가능합니다.
그러나 매우 복잡한 내부 포인터를 사용하고 프로그래머가 모든 구조를 이해해야만 프로그램을 작성할 수 있다는 단점이 여전히 존재합니다.
관계형(Relational) DBMS
1969년 에드거 F.코드(Edgar F. Codd)가 수학 모델에 근거하여 고안했습니다.
관계형(Relational) DBMS의 핵심 개념은 ‘데이터베이스는 테이블(table)’이라는 최소 단위로 구성되어 있으며, 이 테이블은 하나 이상의 열로 구성되어 있다는 것입니다.
관계형 DBMS에서는 모든 데이터가 테이블에 저장됩니다.
테이블이라는 구조는 관계형 DBMS의 가장 기본적이고 중요한 구성으로, 테이블을 잘 이해하면 관계형 DBMS의 기본적인 것을 이해했다고 말할 수 있습니다.
테이블은 데이터를 효율적으로 저장하기 위한 구조입니다.
관계형 DBMS에서는 데이터를 하나가 아닌 여러 개의 테이블에 나누어 저장하므로 불필요한 공간의 낭비를 줄이고 데이터 저장의 효율성을 보장합니다.
이렇게 나뉜 테이블의 관계를 “기본키(Primary Key, PK)” 와 “외래키(Foreign Key, FK)” 를 사용하여 맺음으로써 두 테이블을 부모와 자식 관계로 묶습니다.
그리고 부모와 자식 관계로 연결된 테이블을 서로 조합하여 원하는 결과를 얻을 수 있습니다.
이 때 “SQL(Structured Query Language, 구조화된 질의 언어)” 의 조인(join) 기능을 이용합니다.
테이블은 릴레이션(relation), 엔티티(entity) 등으로도 불립니다.
관계형 DBMS는 다른 DBMS에 비해 업무 변화에 따라 바로 순응할 수 있고 유지﹒보수 측면에서도 편리하다는 특징이 있습니다.
또한 대용량 데이터를 체계적으로 관리할 수 있고 데이터의 무결성도 잘 보장됩니다.
따라서 동시에 데이터에 접근하는 여러 응용 프로그래밍을 사용할 때 관계형 DBMS는 적절한 선택이 될 수 있습니다.
관계형 DBMS의 단점으로는 시스템 자원을 많이 차지하여 시스템이 전반적으로 느려진다는 것을 꼽을 수 있습니다.
그러나 최근에는 하드웨어의 급속한 발전으로 이러한 단점이 많이 보완되고 있습니다.
-
-
-
☕️[Java] 메서드.
메서드의 필요성을 알기위해서 아래의 코드를 보고 실제로 느껴보겠습니다.
아래의 코드는 두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을하는 프로그램입니다.
먼저 1+2를 수행하고, 그 다음으로 10 + 20을 수행합니다.
package method;
public class Method1 {
public static void main(String[] args) {
// 계산1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
System.out.println("결과1 출력: " + sum1);
// 계산2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
System.out.println("결과2 출력:" + sum2);
}
}
위 코드는 다음과 같은 특징이 있습니다.
같은 연산을 두 번 수행합니다.
코드를 잘보면 계산 1 부분과, 계산 2 부분이 거의 같습니다.
계산 1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
계산 2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
계산 1과 2 둘 다 변수를 두 개 선언하고, 어떤 연산을 수행하는지 출력하고, 두 변수를 더해서 결과를 구합니다.
만약 프로그램의 여러 곳에서 이와 같은 계산을 반복해야 할 경우에는 같은 코드를 여러번 반복해서 작성해야 할 것입니다.
더 나아가서 어떤 연산을 수행하는지 출력하는 부분을 변경하거나 또는 제거하고 싶다면 해당 코드를 다 찾아다니면서 모두 수정해야 할 것 입니다.
함수(Function)
함수 정의
add(a, b) = a + b
이름이 add이고, a,b라는 두 값을 받는 함수입니다. 그리고 이 함수는 a + b 연산을 수행합니다.
함수 사용
add(1,2) -> 결과: 3
add(5,6) -> 결과: 11
add(3,5) -> 결과: 8
함수에 값을 입력하면, 함수가 가진 연산을 처리한 다음 결과를 출력합니다. 여기서는 단순히 a + b라는 연산을 수행합니다.
여러번 같은 계산을 해야 한다면 지금처럼 함수를 만들어두고(정의), 필요한 입력 값을 넣어서 해당 함수를 호출하면 됩니다. 그러면 계산된 결과가 나옵니다.
함수는 마치 마술상자와 같습니다. 함수를 호출할 때는 외부에서는 필요한 값만 입력하면 됩니다. 그러면 계산된 결과가 출력됩니다.
내부 구조는 어떻게 되어있는지 알 필요가 없습니다.
같은 함수를 다른 입력 값으로 여러번 호출할 수 있습니다.
여기서 핵심은 함수를 한 번 정의해두면 계속해서 재사용할 수 있다는 점입니다!
평균 함수
만약 두 수의 평균을 구해야 한다면 매번 (a + b) / 2라는 공식을 사용해야 할 것입니다.
이것을 함수로 만들어두면 다음과 같이 사용할 수 있습니다.
함수 정의
avg(a, b) = (a + b) / 2
함수 사용
avg(4, 6) -> 결과: 5
avg(10, 20) -> 결과: 15
avg(100, 200) -> 결과: 150
수하의 함수의 개념을 프로그래밍에 가지고 온다면 어떨까요?
필요한 기능을 미리 정의해두고 필요할 때 마다 호출해서 사용할 수 있기 때문에 앞서 고민한 문제들을 해결할 수 있을 것 같습니다.
프로그램 언어들은 오래 전 부터 이런 문제를 해결하기 위해 수학의 함수라는 개념을 차용해서 사용합니다.
-
-
-
☕️[Java] 메서드(2)
메서드 정의
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
위 코드가 바로 메서드입니다.
이것을 함수를 정의하는 것과 같이, 메서드를 정의한다고 표현합니다.
메서드는 수학의 함수와 유사하게 생겼습니다.
함수에 값을 입력하면, 어떤 연산을 처리한 다음에 결과를 반환합니다.
수학에 너무 집중하지 않아도 됩니다, 단순히 무언가 정의해두고 필요할 때 불러서 사용한다는 개념으로 이해하면 충분합니다.
메서드는 크게 “메서드 선언” 과 “매서드 본문” 으로 나눌 수 있습니다.
메서드 선언(Method Declaration)
public static int add(int a, int b)
메서드의 선언 부분으로, 메서드 이름, 반환 타입, 파라미터(매개변수) 목록을 포함합니다.
이름 그대로 이런 메서드가 있다고 선언하는 것입니다.
메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있습니다.
public static
public: 다른 클래스에서 호출할 수 있는 메서드라는 뜻입니다. (접근 제어에서 학습할 예정)
static: 객체를 생성하지 않고 호출할 수 있는 정적 메서드라는 뜻입니다. (자세한 내용은 추후에 정리)
int add(int a, int b)
int: 반환 타입을 정의합니다. 메서드의 실행 결과를 반환할 때 사용할 반환 타입을 지정합니다.
add: 메서드의 이름입니다. 이 이름으로 메서드를 호출할 수 있습니다.
(int a, int b): 메서드를 호출할 때 전달하는 입력 값을 정의합니다. 이 변수들은 해당 메서드 안에서만 사용됩니다. 이렇게 메서드 선언에 사용되는 변수를 영어로 파라미터(parameter), 한글로 매개변수라 합니다.
메서드 본문(Method Body)
{
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드가 수행해야 하는 코드 블록입니다.
메서드를 호출하면 메서드 본문이 순서대로 실행됩니다.
메서드 본문은 마술상자입니다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모르기 때문입니다.
메서드의 실행 결과를 반환하려면 return문을 사용해야 합니다. return문은 다음에 반환할 결과를 적어주면 됩니다.
return sum: sum 변수에 들어있는 값을 반환합니다.
메서드 호출
앞서 정의한 메서드를 호출해서 실행하려면 메서드 이름에 입력 값을 전달하면 됩니다. 보통 메서드를 호출한다고 표현합니다.
int sum1 = add(5, 10);
int sum2 = add(15, 20);
메서드를 호출하면 어떻게 실행되는지 순서대로 확인해봅시다.
int sum1 = add(5, 10); // add라는 메서드를 숫자 5, 10을 전달하면서 호출합니다.
int sum1 = 15; // add(5, 10)이 실행됩니다. 실행 결과 반환 값은 15입니다.
메서드를 호출하면 메서드는 계산을 끝내고 결과를 반환합니다.
쉽게 이야기하자면, 메서드 호출이 끝나면 해당 메서드가 반환한 결과 값으로 치환됩니다.
메서드 호출이 끝나면 더 이상 해당 메서드가 사용한 메모리를 낭비할 이유가 없습니다.
메서드 호출이 끝나면 메서드 정의에 사용한 파라미터 변수인 int a, int b는 물론이고, 그 안에서 정의한 int sum도 모두 제거 되기 때문입니다.
메서드 호출과 용어정리
메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 합니다.
물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 합니다.
호출: call("hello", 20)
메서드 정의: int call(String str, int age)
인수(Argument)
여기서 hello,20 처럼 넘기는 값을 영어로 Argument(아큐먼트), 한글로 인수 또는 인자라 합니다.
실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용합니다.
매개변수(Parameter)
메서드를 정의할 때 선언한 변수인 String str, int age를 매개변수, 파라미터라 합니다.
메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입됩니다.
실무에서는 매개변수, 파라미터 용어를 모두 사용합니다.
용어정리
인수라는 용어는 ‘인’과 ‘수’의 합성어로, ‘들어가는 수’라는 의미를 가집니다. 즉, 메서드 내부로 들어가는 값을 의미합니다. 인자도 같은 의미입니다.
매개변수, parameter는 ‘매개’와 ‘변수’의 합성어로 ‘중간에서 전달하는 변수’라는 의미를 가집니다. 즉, 메서드 호출부와 메서드 내부 사이에서 값을 전달하는 역할을 하는 변수라는 뜻입니다.
-
🍃[Spring Boot] 스프링?
Intro.
스프링 프레임워크(Spring Framework) 는 자바(Java) 가반의 애플리케이션 프레임워크로 엔터프라이즈급 애플리케이션을 개발하기 위한 다양한 기능을 제공합니다.
스프링은 목적에 따라 다양한 프로젝트를 제공하는데, 그중 하나가 스프링 부트(Spring Boot) 입니다.
이번 포스팅에서는 먼저 스프링 부트의 기반인 스프링 프레임워크를 알아보고, 스프링이 제공하는 다양한 프로젝트 중 하나인 스프링 부트의 특징을 설명하겠습니다.
스프링 프레임워크.
스프링 프레임워크(이후 스프링) 는 자바에서 가장 많이 사용하는 프레임워크입니다.
스프링은 자바 언어를 이용해 엔터프라이즈급 개발을 편리하게 만들어주는 ‘오픈소스 경량급 애플리케이션 프레임워크’로 불리고 있습니다.
쉽게 말해서 자바로 애플리케이션을 개발하는 데 필요한 기능을 제공하고 쉽게 사용하도록 돕는 도구입니다.
TIP: ‘엔터프라이즈급 개발?’
‘엔터프라이즈급 개발’은 기업 환경을 대상으로 하는 개발을 뜻합니다.
네이버나 카카오톡 같은 대규모 데이터를 처리하는 환경을 엔터프라이즈 환경이라고 부릅니다.
스프링은 이 환경에 알맞게 설계되어 있어 개발자는 애플리케이션을 개발할 때 많은 요소를 프레임워크에 위임하고 비즈니스 로직을 구현하는 데 집중할 수 있습니다.
스프링의 핵심 가치는 “애플리케이션 개발에 필요한 기반을 제공해서 개발자가 비즈니스 로직 구현에만 집중할 수 있게끔 하는 것” 입니다.
제어 역적(IoC)
일반적인 자바 개발의 경우 객체를 사용하기 위해 아래의 예제 코드와 같은 코드를 사용합니다.
@RestController
public class NoDIController {
private MyService service = new MyServiceImpl();
@GetMapping("/no-di/hello")
public String getHello() {
return service.getHello();
}
}
즉, 사용하려는 객체를 선언하고 해당 객체의 의존성을 생성한 후 객체에서 제공하는 기능을 사용합니다.
객체를 생성하고 사용하는 일련의 작업을 개발자가 직접 제어하는 구조입니다.
하지만 제어 역전(IoC: Inversion of Controller) 을 특징으로 하는 스프링은 기존 자바 개발 방식과 다르게 동작합니다.
IoC를 적용한 환경에서는 사용할 객체를 직접 생성하지 않고 객체의 생명주기 관리를 외부에 위임합니다.
여기서 ‘외부’ 는 스프링 컨테이너(Spring Container) 또는 IoC 컨테이너(IoC Container) 를 의미합니다.
“객체의 관리를 컨테이너에 맡겨 제어권이 넘어간 것”을 제어 역전이라고 부르며, 제어 역전을 통해 의존성 주입(DI: Dependency Injection), 관점 지향 프로그래밍(AOP: Aspect-Oriented Programming) 등이 가능해집니다.
스프링 을 사용하면 객체의 제어권을 컨테이너로 넘기기 때문에 “개발자는 비즈니스 로직을 작성하는 데 더 집중” 할 수 있습니다.
의존성 주입(DI)
의존성 주입(DI: Dependency Injection)이란 “제어 역전의 방법 중 하나”로, 사용할 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용하는 방식을 의미합니다.
스프링에서 의존성을 주입받는 방법은 3가지가 있습니다.
생성자를 통한 의존성 주입
필드 객체 선언을 통한 의존성 주입
setter 메서드를 통한 의존성 주입
스프링에서는 @Autowired라는 어노테이션(annotation)을 통해 의존성을 주입할 수 있습니다.
스프링 4.3 이후 버전은 생성자를 통해 의존성을 주입할 때 @Autowired 어노테이션을 생략할 수도 있습니다.
하지만 스프링을 처음 다룰 때는 가독성을 위해 어노테이션을 명시하기를 권장합니다.
스프링에서 의존성을 주입받는 각 방법에 대한 예시 코드는 아래와 같습니다.
// 생성자를 통한 의존성 주입
@RestController
public class DIController {
// <-- 의존성을 주입 받는 주요부분
MyService myService;
@Autowired
public DIController(MyService myService) {
this.myService = myServicel
}
// -->
@GetMapping("di/hello")
public String getHello() {
return myService.getHello();
}
}
// 필드 객체 선언을 통한 의존성 주입
@RestController
public class FieldInjectionController {
// <-- 의존성을 주입 받는 주요부분
@Autowired
private MyService myService;
// -->
}
// setter 메서드를 통한 의존성 주입
@RestController
public class SetterInjectionController {
// <-- 의존성을 주입 받는 주요부분
MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
// -->
}
스프링 공식 문서에서 권장하는 의존성 주입 방법은 “생성자를 통해 의존성을 주입받는 방식” 입니다.
다른 방식과는 다르게 생성자를 통해 의존성을 주입받는 방식은 “레퍼런스 객체 없이는 객체를 초기화할 수 없게 설계할 수 있기 때문입니다.”
관점 지향 프로그래밍(AOP)
관점 지향 프로그래밍(이후 AOP: Aspect-Oriented Programming) 은 스프링의 아주 중요한 특징입니다.
AOP는 OOP를 더욱 잘 사용하도록 돕는 개념으로 보는 것이 좋습니다.
스터디 가이드
OOP를 요약하자면 각 기능을 재사용 가능한 개별 객체로 구성해 프로그래밍하는 것을 뜻합니다.
다음과 같은 OOP의 핵심키워드를 이해한다면 더 나은 객체지행 프로그래밍이 가능합니다.
추상화(abstraction)
캡슐화(encapsulation)
상속(inheritance)
다형성(polymorphism)
“AOP는 관점을 기준으로 묶어 개발하는 방식을 의미합니다.”
여기서 “관점(aspect)” 이란 “어떤 기능을 구현할 때 그 기능을 ‘핵심 기능’과 ‘부가 기능’으로 구분해 각각을 하나의 관점으로 보는 것을 의미” 합니다.
“핵심기능”
비즈니스로직을 구현하는 과정에서 비즈니스 로직이 처리하려는 목적 기능을 말합니다.
예를 들면, 클라이언트로부터 상품 정보 등록 요청을 받아 데이터베이스에 저장하고, 그 상품 정보를 조회하는 비즈니스 로직을 구현한다면
(1) 상품 정보를 데이터베이스에 저장하고,
(2) 저장된 상품 정보 데이터를 보여주는 코드가 핵심 기능입니다.
그런데 실제 애플리케이션을 개발할 때는 핵심 기능에 부가 기능을 추가할 상황이 생깁니다.
“핵심 기능인 비즈니스 로직 사이에 로깅 처리를 하거나 트랜잭션을 처리하는 코드를 예로 들 수 있습니다.”
일반적인 OOP 형식으로 비즈니스 로직을 작성하면 아래 그림과 같이 비즈니스 동작 흐름이 발생합니다.
OOP 방식의 애플리케이션 로직에서는 위 그림과 같이 객채마다 핵심 기능을 수행하기 위한 “로직” 과 함께 부가 기능인 “로깅”, “트랜잭션” 등의 코드를 작성합니다.
위 그림의 상품정보 등록 기능과 상품정보 조회 기능은 엄연히 다른 기능으로, 각자 로직이 구현돼 있습니다.
하지만 유지보수 목적이나 데이터베이스 접근을 위해 작성된 “로깅” 과 “트랜잭션” 영역은 상품정보를 등록할 때나 상품정보를 조회할 때 동일한 기능을 수행할 확률이 높습니다.
즉, 핵심 기능을 구현한 두 로직에 동일한 코드가 포함된다는 것을 의미합니다.
AOP의 관점에서는 부가 기능은 핵심 기능이 어떤 기능인지에 구관하게 로직이 수행되기 전 또는 후에 수행되기만 하면 됩니다.
그래서 아래 그림과 같은 구성으로 만들 수 있습니다.
이처럼 여러 비즈니스 로직에서 반복되는 부가 기능을 하나의 “공통 로직으로 처리하도록 모듈화해 삽입하는 방식” 을 “AOP” 라고 합니다.
이러한 AOP를 구현하는 방법은 크게 세 가지가 있습니다.
컴파일 과정에 삽입하는 방식
바이트코드를 메모리에 로드하는 과정에 삽입하는 방식
프락시 패턴을 이용한 방식
이 가운데 스프링은 디자인 패턴 중 하나인 “프락시 패턴” 을 통해 “AOP” 기능을 제공하고 있습니다.
스프링 AOP의 목적은 OOP와 마찬가지로 모듈화해서 재사용 가능한 구성을 만드는 것이고, 모듈화된 객체를 편하게 적용할 수 있게 함으로써 개발자가 비즈니스 로직을 구현하는 데만 집중할 수 있게 도와주는 것입니다.
스프링 프레임워크의 다양한 모듈
스프링 프레임워크는 기능별로 구분된 약 20여 개의 모듈로 구성돼 있습니다.
아래 그림은 스프링 공식 문서에서 제공하는 다이어그램입니다.
스프링 프레임워크 공식 문서에서는 스프링 버전별로 다른 다이어그램을 제시하고 있지만 큰 틀은 유사합니다.
그리고 스프링 프레임워크를 사용한다고 해서 모든 모듈을 사용할 필요는 없습니다.
애플리케이션 개발에 필요한 모듈만 선택해서 사용하게끔 설계돼 있으며, 이를 “경량 컨테이너 설계”라고 부릅니다.
-
-
-
-
-
-
-
-
☕️[JAVA] Packaing 옵션.
☕️[JAVA] Packaing 옵션.
Spring Initializr에서 "Packaing" 옵션을 선택할 때 'Jar'와 'War' 중에 선택해야 합니다.
어떤 것을 선택해야 할지는 개발하려는 어플리케이션의 유형과 배포 환경에 따라 달라집니다.
각 포맷에 대한 설명.
1️⃣ Jar (Java Archive)
Jar 파일은 Java 클래스 파일, 메타데이터, 리소스 파일을 하나의 파일로 압축한 포맷입니다.
스탠드얼론(Spring Boot 어플리케이션 권장 포맷): Jar 포맷은 내장된 서버(예: Tomcat, Jetty)를 사용하여 스프링 부트 어플리케이션을 스탠드얼론 어플리케이션으로 실행할 수 있게 합니다. 이는 별도의 웹 서버 설치 없이도 실행 가능하며, 마이크로서비스, 클라우드 어플리케이션 개발에 적합합니다.
간편한 배포와 실행: Jar 파일은 'java -jar' 명령어로 쉽게 실행할 수 있으며, 도커 컨테이너와 같은 환경에 배포하기도 용이합니다.
2️⃣ War (Web Application Archive)
War 파일은 웹 어플리케이션에 필요한 Java 클래스 파일, JSP(JavaServer Pages), 서블릿, 리소스 파일, 메타데이터 등을 포함한 포맷입니다.
전통적인 웹 어플리케이션: War 포맷은 서블릿 컨테이너나 어플리케이션 서버(예: Tomcat, Jetty, WebLogic, WildFly)에 배포될 전통적인 웹 어플리케이션 개발에 사용됩니다. 이 경우, 어플리게이션 서버가 웹 어플리케이션을 실행하는데 필요한 환경을 제공합니다.
엔터프라이즈 환경: 복잡한 엔터프라이즈 환경에서는 여러 어플리케이션을 하나의 서버에 배포해야 할 필요가 있을 수 있으며, War 포맷이 이러한 요구 사항을 충족시킬 수 있습니다.
🙌 선택 기준
스탠드얼론 어플리케이션 개발 및 마이크로 아키텍처를 선호한다면 'Jar'를 선택하세요.
기존의 엔터프라이즈 환경에서 어플리케이션 서버를 사용해야 한다면 'War'를 선택하세요.
Spring Boot는 두 가지 포맷 모두를 지원하므로, 프로젝트 요구 사항과 배포 환경에 맞게 최적의 옵션을 선택할 수 있습니다.
-
📚[Book] The old man and the sea (7).
📚[Book] The old man and the sea (7).
sardines: 정어리
*"So I can get the cast net and go after the sardines"
*"그래서 나는 투망을 가져다가 정어리를 잡으러 갈 수 있어요"
hard: 단단한,견고한
barided: 땋아진, 삼줄로 엮어진
hard-barided line: 단단하게 땋아진
harpoon: (고래나 큰 물고기를 잡는데 사용되는) 창
*The old man carried mast on his shoulder and the boy carried the wooden box with the coiled, hard-barided lines, the gaff and the harpoon with the its shaft.
*노인은 돛대를 어깨에 메고, 소년은 감겨 있고 단단하게 엮인 갈색 줄, 갈고리대, 그리고 창과 그 손잡이가 담긴 나무 상자를 들고 갔다.
stern: 선미
subdue: 제압하다, 통제하다
*The box with the baits was under the stern of the skiff along with the club that was used to subdue the big fish when they wew brought alongside
*미끼 상자는 보트의 선미 아래에 있었고, 큰 물고기를 옆으로 끌어당겼을 때 그것들을 제압하기 위해 사용된 몽둥이도 함께 있었다.
dew: 이슬
through: ~을 통하여, ~동안, 끝까지
temptation: 유혹
*No one would steal from the old man but it was better to take the sail and the heavy lines home as the dew was bad for them and, though he was quite sure no local people would steal from him, the old man thouhjt that a gaff and a harpoon were needless temptations
아무도 그 노인에게서 훔치지 않겠지만, 이슬이 돛과 무거운 줄들에게 해로웠기 때문에 그것들을 집에 가져가는 것이 나았고, 비록 현지 사람들이 자신에게서 훔치지 않을 것이라고 확신하고 있었지만, 노인은 갈고리대와 창은 불필요한 유혹이라고 생각했다.
nearly: 거의, 대략
*The mast was nearly as long as the one room of the shack.
*돛대는 오두막의 한 방만큼이나 거의 길었다.
budshields: 봉오리 껍질
royal plam: (야자수의 한 종류) 로열 팜
*The shack was made of the tough budshields of the royal palm which are called guano and in it there was a bed, a table, one chair, and a place on the dirt floor to cook with charcoal.
오두막은 로열 팜의 튼튼한 봉오리 껍질로 만들어졌으며, 이것을 구아노라고 부릅니다. 그안에는 침대, 탁자, 의자 하나 그리고 숯으로 요리할 수 있는 흙바닥 위의 공간이 있습니다.
-
-
-
-
-
-
-
🌐 [Network, AWS] Subnet이란?
🌐 [Network, AWS] Subnet이란?
서브넷(Subnet 또는 Subnetwork)은 IP 네트워크를 더 작은, 관리 가능한 부분으로 나누는 방법입니다.
서브네팅은 효율적인 IP 주소 관리, 네트워크 트래픽의 분리 및 제어, 보안 강화를 위해 널리 사용됩니다.
네트워크를 서브넷으로 분할하면 네트워크의 복잡성을 줄이고, 네트워크 성능을 최적화하며, 보안을 강화할 수 있습니다.
서브넷의 주요 개념
IP 주소 할당: 네트워크를 서브넷으로 나누면 각 서브넷에 고유한 IP 주소 범위가 할당됩니다. 이를 통해 네트워크 내에서 트래픽을 효과적으로 라우팅할 수 있습니다.
네트워크 마스크: 서브넷을 식별하기 위해 IP 주소와 함꼐 사용되는 네트워크 마스크(또는 서브넷 마스크)가 있습니다. 네트워크 마스크는 IP 주소의 어느 부분이 네트워크 주소에 해당하고 어느 부분이 호스트 주소에 해당하는지를 정의합니다.
브로드캐스트 도메인 분할: 서브네팅을 사용하면 네트워크의 브로드캐스트 도메인을 분할하여 네트워크 트래픽을 줄이고 성능을 향상시킬 수 있습니다. 각 서브넷은 독립된 브로드캐스트 도메인을 형성합니다.
보안과 관리: 서브넷은 네트워크 리소스에 대한 접근을 제어하는 데 사용될 수 있으며, 네트워크 내의 세그먼트를 보다 쉽게 관리하고 모니터링할 수 있게합니다.
서브넷 사용 예시
기업 네트워크: 기업은 다양한 부서나 기능별로 서브넷을 구성하여 네트워크 리소스를 효율적으로 관리하고 보안을 강화할 수 있습니다.
공용 및 프라이빗 클라우드 환경: 클라우드 환경에서는 VPC 내에서 여러 서브넷을 구성하여 고용 서비스와 프라이빗 리소스를 분리할 수 있습니다.
IoT 네트워크: IoT(Internet Of Things) 환경에서는 서브넷을 사용하여 다양한 유형의 장치를 분리하고, 네트워크 트래픽을 관리하며, 보안을 강화할 수 있습니다.
서브넷 구성은 네트워크 설계의 중요한 부분이며, 네트워크의 규모와 복잡성에 따라 다양한 방식으로 구현될 수 있습니다.
-
-
-
-
-
-
-
-
📚[Book] The old man and the sea.
📚[Book] The old man and the sea.
skiff: 작은 보트
Gulf Stream: 맥시코 만류
*He was an old man who fished alone in a skiff in the Gulf Stream and he had gone eighty-four days now without taking a fish.
*그는 맥시코 만류에서 작은 보트를 타고 혼자 낚시를 하는 노인이었는데, 지금까지 84일 동안 물고기를 한 마리도 잡지 못한 채 지내고 있었습니다.
salao: 최악의 불운한 상태를 뜻하는 스페인어
gaff: 갈고릿대
harpoon: 작살
mast: 돛대
furled: 감다, 감겨 오르다, 펄럭이는
permanent: 영구적인
*It made the boy sad to see the old man come in each day with his skiff empty and he always went down to help him carry either the coled lines or the gaff and harpoon and the sail that was furled around the mast.
*노인이 매일 빈 배를 들고 들어오는 것을 보고 소년은 슬펏고, 그는 항상 내려가서 낚싯줄이나 작살, 돛대 주위에 휘감긴 돛을 나르는 것을 도왔습니다.
sail: 돛
sacks: 자루, 마대
*The sail was patched with flour sacks and, furled, it looked like the flag of permanent defeat.
돛에는 밀가루 자루가 덧대어져 있었고, 펼쳐져 있으면 마치 영원한 패배를 알리는 깃발처럼 보였습니다.
이 문장에서 'fruled'는 '휘감긴, 감다'가 아닌 '펄럭이는, 펼펴져 있는'으로 해석되었습니니다.
gaunt: 쓸쓸한, 수척한.
*The old man was thin and gaunt woth deep wrinkles in the back of his neck.
*그 노인은 목덜미에 깊은 주름이 있고 마르고 여위었습니다.
benevolent: 자애로운
blotches: 얼룩
*The brown blotches of the benevolent skin cancer the sun brings from its reflection on the tropic sea were on his cheeks.
*열대 바다의 반사로 태양이 가져다주는 자비로운 피부암의 갈색 얼룩들이 그의 볼에 있었다.
-
-
🌐[Network] HTTP 통신.
🌐[Network] HTTP 통신이란?
HTTP(HyperText Transfer Protocol) 통신은 월드 와이드 웹(World Wide Web)에서 데이터를 주고받는 데 사용되는 주요 프로토콜입니다.
이 프로토콜은 웹 서버와 클라이언트(대게 웹 브라우저)간의 통신을 위해 설계되었습니다.
🌐 HTTP 통신의 주요 특징.
1. 클라이언트-서버 모델 : HTTP는 클라이언트-서버 모델을 따릅니다.
클라이언트(예: 웹 브라우저)는 서버에 요청(Request)을 보내고, 서버는 이에 대한 응답(Response)을 반환합니다.
2. 무상태성(Stateless) : HTTP는 무상태 프로토콜입니다.
즉, 각 요청은 독립적이며, 서버는 이전 요청에 대한 정보를 저장하지 않습니다.
이는 통신을 단순화하지만, 세션 관리를 위해 쿠기와 같은 메커니즘을 사용해야 합니다
3. HTTP 메소드 : HTTP는 다양한 메소드(GET, POST, PUT, DELETE 등)를 사용하여 리소드(웹 페이지, 이미지, 파일 등)에 대한 다양한 작업을 수행할 수 있습니다.
4. 확장 가능 : HTTP 헤더를 통해 프로토콜을 확장할 수 있습니다.이를 통해 메타데이터, 캐싱 정책, 인증 정보 등을 전송할 수 있습니다.
🌐 HTTP 작동 원리.
1. 요청 시작 : 사용자가 웹 브라우저에서 URL을 입력하거나 링크를 클릭하면 HTTP 요청이 시작됩니다.
2. 서버로의 요청 전송 : 웹 브라우저는 해당 서버의 주소를 찾고(도메인 이름 시스템을 통해 IP 주소를 확인)해당 서버에 연결하여 HTTP 요청을 전송합니다.
3. 서버 처리 및 응답 : 웹 서버는 요청을 받고 처리한 뒤, 요청된 리소스(HTML 페이지, 이미지, 파일 등) 또는 오류 메시지, 리디렉션 정보 등을 포함하는 HTTP 응답을 보냅니다.
4. 콘텐츠 렌더링 : 클라이언트(웹 브라우저)는 응답을 받고, 그 내용을 해석하여 사용자에게 표시합니다. 예를 들어, HTML 문서가 반환되면 브라우저는 이를 파싱하여 화면에 웹 페이지로 렌더링합니다.
5. 연결 종료 : 통신이 완료되면 TCP 연결이 종료됩니다.
HTTP/1.1에서는 지속 연결(keep-alive)을 통해 여러 요청과 응답을 같은 연결로 처리할 수 있습니다.
🌐 HTTP 버전.
HTTP/1.x : 가장 널리 사용되는 버전으로, 각 요청/응답마다 별도의 연결을 맺습니다(HTTP/1.0) 또는 지속 연결을 사용합니다(HTTP/1.1)
HTTP/2 : 성능 향상을 위해 도입된 버전으로, 여러 요청을 동시에 하나의 연결로 처리할 수 있는 멀티플렉싱, 헤더 압축 등의 기능을 제공합니다.
HTTP/3 : 최신 버전으로, UDP 기반의 QUIC 프로토콜을 사용하여 연결의 설정 시간을 단축하고, 패킷 손실에 더 강한 성능을 보입니다
HTTP 통신은 웹의 기본적인 동작 방식을 정의하며, 현대 인터넷에서 가장 중요한 프로토콜 중 하나입니다.
-
-
-
🆙 [LeetCode] 88.Merge Sorted Array.
🆙 [LeetCode] 88.Merge Sorted Array.
Difficulty: Easy
Topic: Array, Two Pointer, Sorting
Approach 1: Merge and sort
Intuition(직관)
순진한 접근 방식은 nums2의 값을 그저 nums1의 끝에 쓰고, 그다음 nums1을 정렬하는 것입니다.
우리는 값을 반환할 필요가 없으며, nums1을 직접 수정해야 합니다.
이 방법은 코딩하기는 쉽지만, 이미 정렬된 상태를 활용하지 않기 때문에 높은 시간 복잡도를 가집니다.
Implementation(구현)
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
for i in 0..<n {
nums1[i + m] = nums2[i]
}
nums1.sort()
}
}
Time complexity(시간 복잡도): O((n + m) log(n+m))
내장된 정렬 알고리즘을 사용하여 길이가 x인 리스트를 정렬하는 비용은 O(xlogx)입니다. 이 경우에는 길이가 m+n인 리스트를 정렬하므로 총 시간 복잡도는 O((n + m) log(n + m))가 됩니다.
Space complexity(공간 복잡도): O(n), 하지만 상황에 따라 다를 수 있습니다.
대부분의 프로그래밍 언어는 O(n) 공간을 사용하는 내장 정렬 알고리즘을 가지고 있습니다.
Approach 2: Three Pointers (Start From the Beginning)
Intuition(직관)
각 배열이 이미 정렬되어 있기 때문에, Two pointer 기법을 활용하면 O(n+m)의 시간 복잡도를 달성할 수 있습니다.
Algorithm
nums1의 값을 복사하여 nums1Copy라는 새 배열을 만드는 것이 가장 간단한 구현 방법입니다.
그런 다음 두 개의 읽기(read) 포인터와 하나의 쓰기(write) 포인터를 사용하여 nums1Copy와 nums2에서 값을 읽고 nums1에 씁니다.
nums1Copy를 nums1의 처음 m 값이 포함된 새 배열로 초기화합니다.
읽기 포인터 p1을 nums1Copy의 시작 부분에 초기화합니다.
읽기 포인터 p2를 nums2의 시작 부분에 초기화합니다.
쓰기 포인터 p를 nums1의 시작 부분에 초기화합니다.
p가 여전히 nums1 내에 있는 동안:
nums1Copy[p1]이 존재하고 nums2[p2] 보다 작거나 같으면:
nums1Copy[p1]을 nums1[p]에 쓰고 p1을 1 증가시킵니다.
그렇지 않으면
nums2[p2]를 nums1[p]에 쓰고 p2를 1 증가시킵니다.
p를 1 증가시킵니다.
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
// nums1의 처음 m개 원소의 복사본을 만듭니다.
let nums1Copy = Array(nums1[0..<m])
// nums1Copy와 nums2에 대한 읽기 포인터입니다.
var p1 = 0
var p2 = 0
// nums1Copy와 nums2에서 원소를 비교하여 더 작은 것을 nums1에 씁니다.
for p in 0..<(m + n) {
// p1과 p2가 각각의 배열 범위를 벗어나지 않도록 확인합니다.
if p2 >= n || (p1 < m && nums1Copy[p1] < nums2[p2]) {
nums1[p] = nums1Copy[p1]
p1 += 1
} else {
nums1[p] = nums2[p2]
p2 += 1
}
}
}
}
Complexity Analysis
Time complexity(시간 복잡도) : O(n+m)
우리는 n+2*m 번의 읽기와 n+2*m 번의 쓰기를 수행하고 있습니다. Big O 표기법에서 상수는 무시되므로, 이는 O(n+m)의 시간 복잡도를 의미합니다.
Space complexity(공간 복잡도) : O(m)
우리는 추가적으로 길이가 m인 배열을 할당하고 있습니다.
Approach 3: Three Pointers (Start From the End)
Intuition
인터뷰 팁: 이것은 쉬운 문제에 대한 중간 수준의 솔루션입니다.
쉬운 수준의 문제 중 상당수는 더 어려운 해결책을 갖고 있으며,
좋은 지원자는 이를 찾을것으로 예상됩니다.
Approach 2는 이미 최상의 시간 복잡도인 O(n+m)을 보여주지만, 여전히 추가 공간을 사용합니다.
이는 nums1 배열의 요소들을 어딘가에 저장해야 하기 때문에, 그것들이 덮어쓰여지지 않도록 해야하기 때문입니다
그렇다면 대신 nums1의 끝부터 덮어쓰기 시작하면 어떨까요? 거기에는 아직 정보가 없으니까요.
알고리즘은 이전과 유사하지만, 이번에는 p1을 nums1의 m - 1 인덱스에, p2를 nums2의 n - 1 인덱스에, 그리고 p를 nums1의 m + n - 1 인덱스에 두는 방식입니다.
이 방식으로, nums1의 처음 m 값들을 덮어쓰기 시작할 때, 이미 각각을 새 위치에 써 놓았을 것이라는 것이 보장됩니다.
이런 방식으로, 추가 공간을 없앨 수 있습니다.
인터뷰 팁: 베열 문제를 제자리에서 해결하려고 할 때는 항상 배열을 앞에서 뒤로 순회하는 대신 뒤에서 앞으로 순회하는 가능성을 고려해보세요
이것은 문제를 완전히 바꾸어 놓고, 훨씩 쉽게 만들 수 있습니다.
Implementation
1️⃣
2️⃣
3️⃣
4️⃣
5️⃣
6️⃣
class Solution {
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
// 각 배열의 끝을 가리키는 p1과 p2를 설정합니다.
var p1 = m - 1
var p2 = n - 1
// p를 배열을 통해 뒤로 이동하면서, 매번 p1 또는 p2가 가리키는 더 작은 값을 작성합니다.
for p in stride(from: m + n - 1, through: 0, by: -1) {
if p2 < 0 {
break
}
if p1 >= 0 && nums1[p1] > nums2[p2] {
nums1[p] = nums1[p1]
p1 -= 1
} else {
nums1[p] = nums2[p2]
p2 -= 1
}
}
}
}
Complexity Analysis
Time complexity: O(n + m)
Same as Approach 2.
Space complexity: O(1)
Unlike Approach 2, we’re not using an extra array.
Proof(optional)
이 주장에 대해 조금 회의적일 수도 있습니다.
정말 모든 경우에 작동하나요?
이렇게 대담한 주장을 하는 것이 안전한가요?
이 방식으로, `nums1`의 처음 `m`개 값을 덮어쓰기 시작하면, 각각을 이미 새 위치에 써 놓았을 것입니다
이런 방식으로 우리는 추가 공간을 없앨 수 있습니다.
훌륭한 질문입니다!
그렇다면 왜 이 방법이 작동할까요?
이를 증명하기 위해, p가 nums1에서 p1이 아직 읽지 않은 값을 덮어쓰지 않는 것을 확실히 해야 합니다.
조언 :증명에 겁을 먹고 있나요?
많은 소프트웨어 엔지니어들이 그렇습니다.
좋은 증명은 간단히 각각의 논리적 주장들이 다음 주장 위에 구축되는 것입니다.
이런 방식으로, 우리는 "명백한" 진술로부터 시작하여 증명하고자 하는 것에 이룰 수 있습니다.
각 진술을 하나씩 읽으며, 다음으로 넘어가기 전에 각각을 이해하는 것이 중요합니다.
초기화 시 p는 p1보다 n만큼 앞서 있다는 것을 알 수 있습니다.(다른 말로, p1 + n = p 입니다.)
또한, 이 알고리즘이 수행하는 p의 반복 동안, p는 항상 1 만큼 감소하고, p1 또는 p2 중 하나가 1 만큼 감소한다는 것도 알고 있습니다.
p1이 감소할 때, p와 p1 사이의 간격은 동일하게 유지되므로, 그 경우에 “추월(overtake)”이 발생할 수 없다는 것을 추론할 수 있습니다.
하지만 p2가 감소할 때는, p는 움직이지만 p1은 그렇지 않으므로, p와 p1 사이의 간격이 1만큼 줄어든가는 것을 추론할 수 있습니다.
그리고 이로부터, p2가 감소할 수 있는 최대 횟수는 n번임을 추론할 수 있습니다. 다시 말해, p와 p1 사이의 간격은 최대 n 만큼 1 씩 줄어들 수 있습니다.
결론적으로, 그들이 처음에 n만큼 떨어져 있었기 때문에 추월이 일어날 수 없습니다. 그리고 p = p1일 때, 간격은 n 번 줄어들어야 합니다. 이는 nums2의 모든 것이 병합되었으므로 더 이상 할 일이 없음을 의미합니다.
-
🆙 [LeetCode] 1089.Duplicate Zeros.
🆙 [LeetCode] 1089.Duplicate Zeros
Difficulty: Easy
Topic: Array, Two Pointers
문제는 배열을 제자리에서 수정하도록 요구합니다.
제자리 수정이 제약 조건이 아니라면, 원본 배열에서 대상 배열로 요소를 복사하는 방법을 사용했을 것입니다.
0을 두 번 복사한 것을 주목하세요.
var s = 0
var d = 0
let N = source.count // 여기서 'source'는 Int 타입의 배열이라고 가정합니다.
var destination = [Int]()
// 목적지 배열이 가득 찰 때까지 복사가 수행됩니다.
while s < N {
if source[s] == 0 {
// 0을 두 번 복사합니다.
destination.append(0)
d += 1
destination.append(0)
} else {
destination.append(source[s])
}
d += 1
s += 1
}
문제 설명에는 새 배열을 확장하지 않고 원래 배열의 길이로만 자른다고도 언급되어 있습니다.
이는 배열의 끝에서 몇몇 요소를 버려야 함을 의미합니다.
이러한 요소들은 새로운 인덱스가 원래 배열의 길이를 넘어서는 요소들입니다.
우리에게 주어진 문제 제약 사항에 대해 다시 생각해 봅시다.
추가 공간을 사용할 수 없기 때문에, 우리의 원본 배열과 대상 배열은 본질적으로 동일합니다.
우리는 단순히 원본을 대상 배열로 그대로 복사할 수 없습니다.
그렇게 하면 몇몇 요소를 잃어버릴 것입니다.
왜냐하면, 우리는 배열을 덮어쓰게 될 것이기 때문입니다.
이를 염두에 두고 아래 겁근 방식에서는 배열의 끝 부분에 복사를 시작합니다.
Approach 1: Two pass, O(1) space
Intuition(직관)
만약 우리가 배열의 끝에서 버려질 요소의 수를 안다면, 나머지는 복사할 수 있습니다.
우리는 어떻게 배열의 끝에서 버려질 요소의 수를 알아낼 수 있을까요?
그 수는 배열에 추가될 여분의 0의 수와 같을 것입니다.
여분의 0은 배열의 끝에서 요소 하나를 밀어내면서 자신을 위한 공간을 만듭니다.
일단 우리가 원래 배열에서 최종 배열의 일부가 될 요소의 수를 알게 되면, 우리는 끝에서부터 복사하기 시작할 수 있습니다.
끝에서부터 복사하는 것은, 마지막 몇 개의 불필요한 요소들을 덮어쓸 수 있기 때문에, 어떤 요소도 잃어버리지 않게 해줍니다.
Algorithm
1️⃣. 중복될 제로의 수를 찾습니다. 이를 possible_dups라고 합시다.
최종 배열의 일부가 되지 않을 잘린 제로들을 세지 않도록 주의해야 합니다.
버려진 제로들은 최종 배열의 일부가 되지 않기 때문입니다.
possible_dups의 개수는 원래 배열에서 잘릴 요소의 수를 알려줄 것입니다.
따라서 어느시점에서든, length_ - possible_dups는 최종 배열에 포함될 요소의 수입니다.
참고: 위의 다이어그램에서는 이해를 돕기 위해 원본 배열과 대상 배열을 보여줍니다
우리는 이러한 연산들을 오직 하나의 배열에서만 수행할 것입니다.
2️⃣. 남은 요소들의 경계에 있는 제로에 대한 에지 케이스를 처리합니다.
이 문제의 에지 케이스에 대해 이야기해 봅시다.
남은 배열에서 제로를 복제할 때는 특별한 주의가 필요합니다.
이 주의는 경계에 놓인 Zero에 대해서 취해져야 합니다.
왜냐하면, 이 제로는 가능한 중복으로 간주되거나, 그것의 복제를 수용할 공간이 없을 때 남은 부분에 포함될 수 있기 때문입니다.
만약 그것이 possible_dups의 일부라면 우리는 그것을 복제하고 싶을 것이고, 그렇지 않다면 복제하지 않을 것입니다.
에지 케이스의 예는 - [8,4,5,0,0,0,0,7] 입니다.
이 배열에서 첫 번째와 두 번째로 제로의 중복을 수용할 공간이 있습니다.
하지만 세번째 제로의 중복을 위한 충분한 공간이 없습니다.
따라서 복사할 때 세 번째 제로에 대해선 두 번 복사하지 않도록 주의해야 합니다.
결과 = [8,4,5,0,`0`,0,`0`,0]
3️⃣. 배열의 끝에서부터 순회하여, 0이 아닌 요소는 한 번, 0 요소는 두 번 복사합니다.
우리가 불필요한 요소들을 버린다고 할 때, 이는 단순히 불필요한 요소들의 왼쪽에서 시작하여 새로운 값들로 그것들을 덮어쓰고, 결국 남은 요소들은 오른쪽으로 이동시켜 배열 안에 중복된 요소들을 위한 공간을 만들어낸다는 것을 의미합니다.
class Solution {
func duplicateZeros(_ arr: inout [Int]) {
var possibleDups = 0
let length_ = arr.count - 1
// 복제할 0의 개수를 찾습니다.
// 원래 배열의 마지막 요소를 넘어서면 중지합니다.
// 수정된 배열의 일부가 될 마지막 요소를 넘어서면 중지합니다.
for left in 0...(length_ - possibleDups) {
// 0 숫자 세기
if arr[left] == 0 {
// Edge case: 이 0은 복제할 수 없습니다. 더 이상 공간이 없습니다.
// 왼쪽은 포함될 수 있는 마지막 요소를 가리키고 있습니다.
if left == length_ - possibleDups {
// 이 0의 경우 중복 없이 복사합니다.
arr[length_] = 0
break
}
possibleDups += 1
}
}
// 새 배열의 일부가 될 마지막 요소부터 거꾸로 시작합니다.
var last = length_ - possibleDups
// 0을 두 번 복사하고 0이 아닌 것을 한 번 복사합니다.
while last >= 0 {
if arr[last] == 0 {
arr[last + possibleDups] = 0
possibleDups -= 1
arr[last + possibleDups] = 0
} else {
arr[last + possibleDups] = arr[last]
}
last -= 1
}
}
}
Complexity Analysis
시간 복잡도(Time Complexity): O(N), 여기서 N은 배열의 요소 수입니다. 우리는 배열을 두 번 순회하는데, 하나는 possible_dups의 수를 찾기 위해, 다른 하나는 요소들을 복사하기 위해 사용됩니다. 최악의 경우, 배열에 zero가 적거나 없을 때 배열 전체를 순회할 수도 있습니다.
공간 복잡도(Space Complexity): O(1), 우리는 추가적인 공간을 사용하지 않습니다.
-
📝 배열 삽입 3(배열의 아무 곳에나 삽입하기 - Inserting Anywhere in the Array)
배열 삽입 시리즈
배열 삽입1 (배열의 끝에 삽입하기-Inserting at the End of an Array)
배열 삽입2 (배열의 시작 부분에 삽입하기 - Inserting at the Start of an Array)
마찬가지로, 주어진 인덱스에 삽입하기 위해서는, 해당 인덱스부터 시작하는 모든 요소들을 오른쪽으로 한 자리씩 이동시켜야 합니다.
새 요소를 위한 공간이 생성되면, 삽입을 진행합니다.
생각해보면, 시작 부분에 삽입하는 것은 사실 주어진 인덱스에 요소를 삽입하는 것의 특별한 경우에 해당합니다.
그 경우에 주어진 인덱스는 0이었습니다.
다시 한 번 말씀드리지만, 이것도 비용이 많이 드는 작업입니다.
새 요소를 실제로 삽입하기 전에 거의 모든 다른 요소들을 오른쪽으로 이동시켜야 할 수도 있기 때문입니다.
위에서 보셨듯이, 많은 요소들을 오른쪽으로 한 칸씩 이동시키는 것은 삽입 작업의 시간 복잡도를 증가시킵니다.
다음은 코드의 모습입니다
// 배열 삽입 1,2 코드 참고
var intArray = [Int](repeating: 0, count: 6)
var length = 0
for i in 0..<3 {
intArray[length] = i
length += 1
}
func printArray() {
for i in 0..<intArray.count {
print("Index \(i) contains \(intArray[i])")
}
}
intArray[length] = 10
length += 1
for i in(0...3).reversed() {
intArray[i + 1] = intArray[i]
}
intArray[0] = 20
// 인덱스 2에 요소를 삽입하고 싶다고 가정해봅시다.
// 먼저, 새로운 요소를 위한 공간을 만들어야 합니다.
for i in stride(from: 4, through: 2, by: -1) {
// 각 요소를 오른쪽으로 한 위치씩 이동시킵니다.
intArray[i + 1] = intArray[i]
}
// 이제 새로운 요소를 위한 공간을 만들었으므로,
// 필요한 인덱스에 삽입할 수 있습니다.
intArray[2] = 30
printArray()
다음은 printArray를 실행한 결과입니다.
Index 0 contains 20.
Index 1 contains 0.
Index 2 contains 30.
Index 3 contains 1.
Index 4 contains 2.
Index 5 contains 10.
주의해야 할 주요한 것은 array.capacity가 베열의 전체 용량을 제공한다는 점을 기억하는 것입니다.
마지막으로 사용된 슬롯을 알고 싶다면 count 변수를 사용하여 직접 추적해야합니다.
-
📝 배열 삽입 2(배열의 시작 부분에 삽입하기 - Inserting at the Start of an Array)
배열 삽입 시리즈
배열 삽입1 (배열의 끝에 삽입하기-Inserting at the End of an Array)
배열의 시작 부분에 삽입하기(Inserting at the Start of an Array)
배열의 시작 부분에 요소를 삽입하려면, 새 요소를 위한 공간을 만들기 위해 배열의 다른 모든 요소들을 오른쪽으로 하나의 인덱스만큼 이동시켜야 합니다.
이것은 비용이 매우 많이 드는 작업입니다, 왜냐하면 기존의 요소들을 모두 오른쪽으로 한 단계씩 이동시켜야 하기 때문입니다.
모든 것을 이동시켜야 한다는 것은 이 작업이 상수 시간 작업이 아니라는 것을 의미합니다.
사실, 배열의 시작 부분에 삽입하는 데 걸리는 시간은 배열의 길이에 비례할 것입니다.
시간 복잡도 분석 측면에서 이는 선형 시간 복잡도, 즉 O(N)인데, 여기서 N은 배열의 길이입니다.
다음은 코드의 모습입니다.
// 배열삽입 1 코드 참고
var intArray = [Int](repeating: 0, count: 6)
var length = 0
for i in 0..<3 {
intArray[length] = i
length += 1
}
func printArray() {
for i in 0..<intArray.count {
print("Index \(i) contains \(intArray[i])")
}
}
intArray[length] = 10
length += 1
// 먼저, 새로운 요소를 위한 공간을 만들어야 합니다.
// 이를 위해 각 요소를 오른쪽으로 하나의 인덱스만큼 이동시킵니다.
// 이것은 먼저 인덱스 3의 요소를 이동시키고, 그 다음 2, 그 다음 1, 마지막으로 0을 이동시킵니다.
// 어떤 요소도 덮어쓰지 않기 위해 뒤에서부터 진행해야 합니다.
for i in(0...3).reversed() {
intArray[i + 1] = intArray[i]
}
// 이제 새로운 요소를 위한 공간을 만들었으므로,
// 시작 부분에 삽입할 수 있습니다.
intArray[0] = 20
printArray()
다음은 printArray()를 실행한 결과입니다.
Index 0 contains 20.
Index 1 contains 0.
Index 2 contains 1.
Index 3 contains 2.
Index 4 contains 10.
Index 5 contains 0.
-
-
-
-
-
📝 배열의 용량 vs 배열의 길이
Array Capacity VS Length
만약 누군가가 당신에게 DVD 배열의 길이가 얼마나 되는지 물어본다면, 당신의 대답은 무엇일까요?
당신은 두 가지 다른 대답을 할 수 있습니다.
상자가 가득 차있을 경우, 상자가 담을 수 있는 DVD의 수, 또는
현재 상자에 들어있는 DVD의 수.
이 두 답변은 모두 정확하며, 매우 다른 의미를 가집니다!
이 둘의 차이를 이해하고 올바르게 사용하는 것이 중요합니다.
우리는 첫 번째를 배열의 ‘용량’이라고 부르고, 두번째를 ‘길이’라고 부릅니다.
Array Capacity
DVD[] array = new DVD[6]
array[6]에 요소를 삽입하는 것이 유효한 작업일까요?
array[10]은 어떨까요?
아니요, 이 두 경우 모두 유효하지 않습니다.
배열을 생성할 때, 이 배열이 최대 6 개의 DVD를 담을 수 있다고 지정했습니다.
이것이 배열의 용량입니다.
인덱싱이 0부터 시작한다는 것을 기억한다면, 우리는 오직 array[0], array[1], array[2], array[3], array[4] 그리고 array[5]에만 항목을 삽입할 수 있습니다.
array[-3], array[6], array[100]과 같이 다른 곳에 요소를 넣으려고 하면 ArrayIndexOutOfBoundsExecption으로 코드가 충돌하게 됩니다.
배열의 용량은 배열이 생성될 때 결정되어야 합니다.
용량은 나중에 변경할 수 없습니다.
우리가 사용한 종이 상자에 DVD를 넣는 비유로 돌아가 보면, 배열의 용량을 변경하는 것은 종이 상자를 더 크게 만들려는 것과 같습니다.
고정된 크기의 종이 상자를 더 크게 만드는 것은 비현실적이며, 컴퓨터의 배열에서도 마찬가지입니다!
그렇다면 7번째 DVD를 얻었을 때, 모든 DVD를 같은 배열에 넣고 싶다면 어떻게 할까요?
불행히도 종이 상자의 경우와 마찬가지입니다.
더 큰 상자를 새로 구해서, 기존의 DVD들과 새로운 것을 모두 옮겨야 합니다
자바에서 배열의 용량은 배열의 length 속성값을 확인함으로써 알 수 있습니다.
이는 arr.length라는 코드를 사용하여 확인되는데, 여기서 arr은 배열의 이름입니다.
다른 프로그래밍 언어들은 배열의 길이를 확인하는 데 다른 방법을 사용합니다.
int capacity = array.length;
System.out.println("The Array has a capacity of " + capacity);
이 코드를 실행하면 다음과 같은 출력이 나옵니다:
The Array has a capacity of 6
capacity property of Swift
Instance Property
capacity
배열이 새로운 저장 공간을 할당하지 않고 담을 수 있는 요소의 총 수입니다.
모든 배열은 그 내용을 저장하기 위해 특정 양의 메모리를 예약합니다.
배열에 요소를 추가하고 그 배열이 예약된 용량을 초과하기 시작하면, 배열은 더 큰 메모리 영역을 할당하고 그 요소들을 새로운 저장 공간으로 복사합니다.
새로운 저장 공간은 기존 저장 공간 크기의 배수입니다.
이 지수적 성장 전략은 요소 추가 작업이 평균적으로 상수 시간 내에 이루어지게 하여, 많은 추가 작업의 성능을 평균화합니다.
재할당을 유발하는 추가 작업에는 성능 비용이 들지만, 배열이 커짐에 따라 그런 작업은 점점 덜 자주 발생합니다.
다음 예시는 배열 리터럴로부터 정수 배열을 생성한 다음, 다른 컬렉션의 요소들을 추가합니다.
추가 하기 전에, 배열은 결과 요소들을 저장할 수 있을 만큼 충분히 큰 새로운 저장 공간을 할당합니다.
var numbers = [10, 20, 30, 40, 50]
// numbers.count == 5
// numbers.capacity == 5
numbers.append(contentsOf: stride(from: 60, through: 100, by: 10))
// numbers.count == 10
// numbers.capacity == 10
Array Length
길이(length) 의 또 다른 정의는 배열에 현재 들어 있는 DVD의 수, 또는 다른 항목들의 수입니다
이것은 직접 추적해야 할 것이며, 기존 DVD를 덮어쓰거나 배열에 공백을 남겨두어도 오류는 발생하지 않습니다.
이전 예제에서 length 변수를 사용하여 다음 비어 있는 인덱스를 추적하고 있는 것을 눈치챘을 수 있습니다.
// 용량이 6인 새 배열을 생성합니다.
int[] array = new int[6];
// 현재 길이는 0이며, 요소가 0개 있기 때문입니다.
int length = 0;
// 그 안에 3개의 항목을 추가합니다.
for (int i = 0; i < 3; i++) {
array[i] = i * i;
// 요소를 추가할 때마다 길이가 1씩 증가합니다.
length++;
}
System.out.println("배열의 용량은 " + array.length + "입니다.");
System.out.println("배열의 길이는 " + length + "입니다.");
이 코드를 실행하면 다음과 같은 출력이 나옵니다:
The Array has a capacity of 6
The Array has a length of 3
count property of Swift
Instance Property
count
배열의 요소(elements) 수 입니다.
var count: Int { get }
Handling Array Parameters(배열 매개변수 처리하기)
LeetCode에서의 대부분의 배열 문제는 “길이”나 “용량” 매개변수 없이 매개변수로 배열을 전달합니다.
이게 무슨 뜻일까요?
예를 들어 설명해 보겠습니다.
여기 당신이 풀게 될 첫 번째 문제의 설명이 있습니다.
‘이진 배열이 주어졌을 때, 이 배열에서 연속된 1의 최대 개수를 찾아라.’
그리고 여기 주어진 코드 템플릿이 있습니다.
class Solution {
public int findMaxConsecutiveOnes(int[] nums) {
}
}
유일한 매개변수는 'nums' 인데, 이는 배열입니다. 'nums'의 길이를 모르면 이 문제를 해결할 수 없습니다.
다행히도 이는 간단합니다.
매개변수로 주어진 배열에 대한 추가 정보가 없을때는 길이 == 용량 (length == capacity) 이라고 안전하게 가정할 수 있습니다.
즉, 배열은 그 데이터를 모두 담기에 정확히 적합한 크기입니다.
따라서 .length를 사용할 수 있습니다.
하지만 조심하세요, 배열은 0부터 시작하는 인덱스입니다.
용량(capacity)/길이(length)는 항목의 수이지 최고 인덱스가 아닙니다.
최고 인텍스는 .lenght -1 입니다.
따라서 배열의 모든 항목을 순회하기 위해 다음과 같이 할 수 있습니다.
class Solution {
public int findMaxConsectiveOnes(int[] nums) {
// 힌트: 여기에 변수를 초기화하고 선언하여
// 연속된 1이 몇 개인지 추적합니다.
for (int i = 0; i < nums.length; i++) {
// nums[i] 요소로 무언가를 합니다.
}
}
}
이것이 바로 시작하기 위해 필요한 배열의 기본 사항입니다!
-
📝 스위프트에 왜 변수가 있을까?
변수는 프로그램에서 임시 정보를 저장하는 데 사용되며, 거의 모든 Swift 프로그램의 핵심 부분을 이룹니다.
여러분의 프로그램은 어떤 식으로든 데이터를 변환할 것입니다: 사용자가 할 일 목록 작업을 입력하고 체크하게 하거나, 황량한 섬에서 자본주의적인 너구리를 위해 돌아다니게 하거나, 기기 시간을 읽고 시계에 표시하는 등입니다.
어쨋든, 어떤 종류의 데이터를 받아 어떤 식으로든 변환하고 사용자에게 보여주는 것입니다.
물론, ‘어떤 식으로든 변환하는’ 부분이 진짜 마법이 일어나는 곳입니다, 왜냐하면 그곳이 여러분의 놀라운 아이디어가 실현되는 곳이기 때문입니다.
하지만 데이터를 메모리에 저장하는 과정 - 사용자가 입력한 것이나 인터넷에서 다운로드한 것을 기억하는 것 - 이 곳이 변수가 사용되는 곳 입니다.
var 를 사용하여 변수를 생성하면, var 를 다시 사용하지 않고도 원하는 만큼 변경할 수 있습니다.
예를 들어:
var favoriteSports = "Tennis"
favoriteSports = "MMA"
favoriteSports = "Crossfit"
만약 도움이 된다면, var 를 “새로운 변수 생성(create a new variable)”으로 읽어보세요.
따라서, 위의 첫 번째 줄은 “새로운 변수 favoriteSports 를 생성하고 그것에 Tennis 값을 주세요”로 읽을 수 있습니다.
두 번째와 세 번째 줄에는 var 가 없으므로, 새 변수를 생성하는 것이 아니라 기존 값을 수정합니다.
이제 모든 세 줄에 var 가 있다고 상상해보세요 - 매번 var favoriteSports 를 사용했습니다.
그것은 많은 의미가 없을 것입니다, 왜냐하면 여러분은 “새로운 변수 favoriteSports 를 생성하라”고 세 번 반복하게 되고, 변수는 첫 번째 시도 후에 분명히 새로운 것이 아닙니다.
Swift는 이것을 오류로 표시할 것이고, 여러분이 변수에 다른 이름을 선택할 때까지 코드를 실행하지 못하게 할 것입니다.
그것이 성가신 행동처럼 보일 수 있지만, 신뢰하세요: 그것은 도움이 됩니다!
Swift는 여러분에게 명확해지길 원합니다: 기존 변수를 수정하려고 하고 있다면(그렇다면 두 번째와 그 이후에는 var 를 제거하세요), 아니면 새 변수를 생성하려고 하는 것인가요?(그 경우에는 다른 이름을 지으세요.)
마지막으로: 변수가 많은 Swift 프로그램의 핵심을 이루고 있지만, 때때로 그것들을 피하는 것이 가장 좋다는 것을 배우게 될 것입니다.
이에 대해서는 나중에 더 자세히 학습하겠습니다
-
Touch background to close