2. 독후감✍️ 정보처리기사와 기술 면접을 대비해서 네트워크 기초를 공부해 본 적은 있지만, 취업 이후에는 네트워크와 관련된 작업을 하지 않다보니 자연스럽게 잊혀졌다. 그러다 올해부터 회사에서 미니 프로젝트를 진행할 것이라는 소식을 들었다. 어떤 내용으로 진행할지 고민하다, 이번 기회에 자주 접하고 있지만 잘 모르는 주제를 선정해서 공부하면 좋을 것 같다고 생각했다. 그리고 그 주제가 네트워크🛜가 됐다. 인터넷을 통해서 정보를 수집하는 것도 좋지만, 정제된 지식이 정리된 책을 읽어보면 좋을 것 같다는 생각에 구입한 책이다(교보문고 기준 네트워크 검색어 + 판매량 순으로 순위권에 보이지 않는 책이지만, 어디선가 추천 글을 보아서 샀다🫠). 개인적으로 추천하는 책이고, 추천하는 이유는 지식을 연결해주기 때문이다. 간단한 예시로 4계층(전송 계층)은 신뢰할 수 있는 통신을 구현하는 계층이라고 외웠었는데, 신뢰할 수 있는 통신을 어떻게 만드는지 쉽고 간단하게 알려준다. 이 책을 읽고 막연하게 '정보는 상호 합의하에.. 잘.. 주고 받고 있지 않을까🫠..?'라는 생각을 '눈에 보이지 않는 전파를 통해 데이터가 이동하고 있구나🤓!'로 바꿀 수 있었다(그래서 가끔은 전파가 늘 이렇게 우리랑 함께해도 건강에 괜찮은가🥸라는 생각도 한다).
3. 새롭게 알게 된 내용📑
웹 서버에서의 데이터 전달과 처리
물리 계층 데이터가 전기 신호로 웹 서버에 도착
데이터 링크 계층 이더넷 프레임의 목적지 MAC 주소와 자신의 MAC 주소 비교 주소가 같으면 이더넷 헤더와 트레일러를 분리하고 네트워크 계층으로 전달
네트워크 계층 목적지의 IP 주소와 웹 서버의 IP 주소가 같은지 확인 주소가 같으면 IP 헤더를 분리하고 전송 계층에 전달
전송 계층 목적지 포트 번호를 확인하여 어떤 애플리케이션으로 전달해야 되는지 판단 TCP 헤더를 분리하여 응용 계층에 전달
응용 계층 전송 계층으로부터 받은 데이터를 애플리케이션에 맞게 해석하고 처리 예: HTTP 요청이 도착하면 웹 서버는 이를 해석하여 적절한 웹 페이지를 반환
2. 독후감✍️ 책이 쉽다. 그리고 MySQL🐬 환경이다. 다음 책은 SQL 레벨업인데, 그건 또 어렵다(그래서 읽는 걸 좀 미뤘다). SQL 기본 지식이 있는 상태(저는 SQLD를 소지하고 있습니다🙋♀️)라면, 이 책을 굳이 읽지 않아도 될 것 같다. 책도 얇고 단원이 잘 나눠져있어서 들고다니며 가볍게 보기 좋을 것 같다. 근데, 쿼리 좀 작성해봤으면 안 읽어도 될 것 같다.
3. 새롭게 알게 된 내용📑
새롭게 알게된 내용이나 알고 있던 내용을 깔끔하게 정리할 수 있도록 도와준 내용🤓!
-- * Null 값의 정렬 순서
-- * MySQL 기준 Null 값을 가장 작은 값으로 취급
-- * Oracle 기준 Null 값을 가장 큰 값으로 취급
-- * LIMIT
-- * 행수 제한
-- * where 조건절과 달리 order by 정렬 이후 최종적으로 수행
-- * OFFSET: 시작 위치 지정(0부터 시작)
select * from sample33 limit 3;
/*
+------+
| no |
+------+
| 1 |
| 2 |
| 3 |
+------+
*/
select * from sample33 limit 3 offset 3;
/*
+------+
| no |
+------+
| 4 |
| 5 |
| 6 |
+------+
*/
2. 독후감✍️ 누군가 내 코드를 읽으며 인상을 찌푸릴 때, 한 번쯤은 속으로 물었던 그 말. '제 코드가 그렇게 이상한가요..?'
코드 품질과 관련된 내용은 어째서인지 신입 개발자에게는 너무 성급한 주제라고 생각했다. 새로운 지식을 한창 쌓아갈 시기에 쌓인 지식을 다듬는 느낌이라 그간 미뤄왔는데, 한 번도 사용하지 않았던 MVVM패턴에 맞춰 기능을 구현하기 바빠 나도 내 코드를 보면서 ' 어😯? ' 하는 경우가 생겨 공부를 시작했다.
교보문고에 "코드"를 검색해서 판매량 기준으로 정렬하면, 좋은 책들과의 경쟁에서 밀려 3페이지 가장 마지막에 머물러있다. 하지만, 이 책은 내가 처음으로 코드 설계와 구조에 대해 관심을 갖게해준 책으로, 주변에 추천하고 싶은 책이다. 읽으면서 중요한 내용은 다시 복기하려고 적어두었는데, 문맥을 잊어버릴까봐 책도 결국은 샀다.
이 글을 수정하고 있는 24년 7월 기준 나는 C#을 쓰고 있는데, Java로 되어있는 예제 코드를 따라 쓰면서 오랜만에 다시 Java를 공부하는 재미도 있었다. 여러 좋은 내용이 있었지만, 프로젝트에서 이 책을 보고 적용해봤던 내용은 조건 분기 부분이었다. 책에서 설명한 조기 return을 활용하는 방법은 아래와 같다.
최근에 Transaction 관련 문제로 코드를 한 번 크게 수정했습니다. 성능 상의 문제로 해당 Transaction은 다시 수정되었지만, 오랜만에 '이게 Transaction의 논리적 단위지🔥!'라는 깨달음을 얻어 정리하고자 합니다✍️.
Transaction
Transaction이란, 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 연산들을 의미합니다. 대표적인 예시로는 은행의 입출금이 있습니다. 내 통장에서 돈을 이체할 경우, 내 통장에서의 출금과 타인 통장에서의 입금은 각각 다른 동작이지만 반드시 한꺼번에 처리 되어야 할 논리적인 기능이 되는 것이지요. 논리적 기능을 어떻게 정의하냐는 사람마다, 상황마다 다릅니다. 예를 들어, 통장의 입출금만 하나의 트랜잭션으로 볼 것인가, 아니면 고객의 통장에 기록을 남기는 것까지 하나의 트랜잭션으로 담을 것인가 등의 다양한 경우가 만들어질 수 있습니다.
Transaction 특징(ACID)
ACID는 트랜잭션의 안전성을 보장하기 위해 필요한 주요한 특징들을 모아놓은 것입니다.
원자성(Atomicity) 트랜잭션의 연산이 모두 데이터베이스에 반영되지 않는다면 전부 반영되서는 안된다는 원칙입니다. One or Nothing과 같은 개념이죠. 이를 통해 데이터의 오염을 방지할 수 있습니다. 예를 들어 크레파스를 사서, 그림을 그린다가 하나의 트랜잭션이라면 크레파스만 사고 끝났다는 없습니다🫠. 크레파스를 사서 그림까지 그리던지, 그림을 안그릴거면 크레파스를 환불해야 합니다🥸. 방금 전은 가벼운 예시지만, 원자성을 지키는 것의 중요하고도 대표적인 예시는 송금입니다. 내 통장에서 돈을 빼서 타인 통장에 입금을 할 경우, 내 통장에서 돈만 빼내는 건 안됩니다☠️. 타인 통장에 입금을 실행하지 못하게 된다면 내 통장에 돈을 다시 돌려줘야 합니다😐.
일관성(Consistency) 트랜잭션 이후에도 이전과 동일하게, 즉 일관되게 데이터베이스의 제약이나 규칙을 만족시켜야 한다는 것입니다. 송금 예시와 연결지어 생각해보면, 데이터베이스에 입금은 반드시 0원보다 커야한다라는 규칙이 있다면 제 통장에서 0원이 출금되고, 타인의 통장에 0원이 입금된다하면 원자성은 만족하지만 일관성은 만족시키지 못하게 됩니다. 트랜잭션 이후 규칙을 만족시키지 않았기 때문이죠.
독립성, 격리성(Isolation) 마음에 드는 정의로 이해하시면 됩니다🫠.
둘 이상의 트랜잭션이 동시에 실행될 경우, 어떤 하나의 트랜잭션이 다른 트랜잭션에 끼어들 수 없다.
수행중인 트랜잭션이 완전히 완료될 때까지 다른 트랜잭션에서 수행 결과를 참조할 수 없다.
트랜잭션이 동시에 실행될 때와 연속으로 실행될 때의 데이터베이스 상태가 동일해야 한다.
트랜잭션의 독립성은 여러 의미로 설명할 수 있습니다. 계속해서 송금을 예시로 설명하자면, A->B : 30만원 송금 B->C: 20만원 송금을 한다고 했을 때, A와 B간 1개의 트랜잭션, B와 C간 1개의 트랜잭션이므로, A, B, C의 관계를 하나로 묶어서 A: 30만원 출금, B: 10만원 입금, C: 20만원 입금을 처리 할 수 없습니다. A->B의 트랜잭션이 끝나지 않았는데, B->C 트랜잭션에서 이 결과를 참조할 수 없기 때문이죠.
지속성(Durability) 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 한다는 것입니다. 송금이 성공적으로 처리되었으면, 시스템(데이터베이스) 상에 기록이 되어야 한다는 의미입니다.
Transaction 연산
Commit 한 개의 트랜잭션이 성공적으로 끝났고, 데이터베이스가 일관된 상태(=일관성)에 있을 때, 트랜잭션의 성공을, 즉 변경이 완료됬음을 트랜잭션 관리자에게 알려주는 연산입니다.
Rollback 트랜잭션이 수행되던 도중 비정상적으로 종료되어 일부만 정상적으로 처리되었을 때, 트랜잭션의 원자성을 구현하기 위해 해당 트랜잭션의 모든 연산을 취소할 것을 트랜잭션 관리자에게 알려주는 연산입니다.
* 특정 IP 주소와 포트에서 클라이언트의 연결 요청을 기다리고, 연결이 수립되면 데이터를 주고받는 역할
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TcpServerTest1
{
internal class Program
{
static void Main(string[] args)
{
NetworkStream stream = null;
TcpListener listener = null;
Socket clientSocket = null;
StreamReader reader = null;
StreamWriter writer = null;
try
{
// 1. 서버 IP 주소 설정
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
// 2. TCPListener 생성 및 시작
// TCPListener: 클라이언트의 연결을 수신
listener = new TcpListener(ipAddress, 5001);
listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작
// 3. 클라이언트의 연결 대기
// 클라이언트의 연결 요청을 받아들여 Socket 객체를 반환
// 클라이언트가 연결을 시도할 때까지 블로킹(즉, 이 코드에서 멈추고 기다림)
clientSocket = listener.AcceptSocket();
// 4. 클라이언트와의 데이터 통신을 위한 스트림 설정
// clientSocket과 연결된 NetworkStream 객체를 생성 -> 이를 통해 클라이언트와 데이터를 주고받을 수 있음
// Encoding: 데이터를 바이트로 변환하거나 바이트를 텍스트로 변환할 때 사용
stream = new NetworkStream(clientSocket);
Encoding encoding = Encoding.GetEncoding("utf-8");
// 5. 데이터 읽기와 쓰기를 위한 스트림 리더와 라이터 생성
reader = new StreamReader(stream, encoding);
writer = new StreamWriter(stream, encoding) { AutoFlush = true };
// 6. 데이터 수신 및 송신 루프
while (true)
{
// 클라이언트로부터 데이터 읽기
string str = reader.ReadLine();
Console.WriteLine(str);
// 클라이언트에게 데이터 응답
writer.WriteLine(str);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
clientSocket.Close();
}
}
}
}
TCP Client
using System.Net.Sockets;
using System.Text;
namespace TcpClientTest1
{
internal class Program
{
static void Main(string[] args)
{
TcpClient client = null;
try
{
// 1. TCPClient 생성
client = new TcpClient();
// 2. 서버에 연결 시도
// 서버가 해당 포트에서 연결 요청을 듣고 있어야 함
client.Connect("localhost", 5001);
// 3. 네트워크 스트림 획득
// 네트워크 상에서 데이터를 스트림 방식으로 송수신하는 기능을 제공
// 스트림: 데이터를 바이트 단위로 처리하며, 데이터를 차례로 읽거나 쓸 수 있는 방식
// 예를 들어, 서버가 클라이언트에 메시지를 보낼 때 NetworkStream을 통해 데이터를 읽을 수 있음
// 클라이언트는 NetworkStream을 통해 서버로 메시지를 전송할 수 있음
NetworkStream stream = client.GetStream();
// 4. 인코딩 설정
Encoding encoding = Encoding.GetEncoding("utf-8");
// 5. 스트림 리더와 라이터 생성
// StreamWriter: 데이터를 서버로 보내는 데 사용, AutoFlush: 매번 쓰기 작업 후에 버퍼를 자동으로 비움
// StreamReader: 서버로부터 데이터를 읽는 데 사용
StreamWriter writer = new StreamWriter(stream) { AutoFlush=true };
StreamReader reader = new StreamReader(stream, encoding);
// 6. 사용자 입력 받기
string dataToSend = Console.ReadLine();
while (true)
{
// 7. 서버에 데이터 전송
writer.WriteLine(dataToSend);
// 8. 종료 조건 체크
if (dataToSend.IndexOf("<EOF>") > -1)
{
break;
}
// 9. 서버로부터 데이터 읽기 및 출력
Console.WriteLine(reader.ReadLine());
// 10. 다음 데이터 입력
dataToSend = Console.ReadLine();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
throw;
}
finally
{
client.Close();
}
}
}
}
TCP Server + Multi Threading
* 기존 TCP Server에서는 Server : Client가 1:1 구조를 이루면서, 클라이언트가 2이상일 때 처리하지 못함
* 여러 클라이언트가 동시에 서버에 연결하고 통신할 수 있도록 멀티스레딩을 활용
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TcpServerTest1
{
// 개별 클라이언트와의 통신을 처리
// 각 클라이언트는 별도의 ClientHandler 인스턴스를 가지며, 이 인스턴스는 별도의 스레드에서 실행
internal class ClientHandler
{
// 클라이언트와의 연결을 나타내는 소켓
Socket socket = null;
// 클라이언트와의 데이터 통신을 위한 스트림
NetworkStream stream = null;
// 클라이언트로부터 데이터를 읽는 리더
StreamReader reader = null;
// 클라이언트에게 데이터를 쓰는 라이터
StreamWriter writer = null;
public ClientHandler(Socket socket)
{
this.socket = socket;
}
// 클라이언트와 데이터를 주고받는 메서드
// 무한 루프를 사용하여 지속적으로 클라이언트로부터 데이터를 읽고 응답
public void Chat()
{
stream = new NetworkStream(socket);
Encoding encoding = Encoding.GetEncoding("utf-8");
reader = new StreamReader(stream, encoding);
writer = new StreamWriter(stream, encoding) { AutoFlush = true };
while (true)
{
// 클라이언트로부터 데이터 읽기
string str = reader.ReadLine();
Console.WriteLine(str);
// 클라이언트에게 데이터 응답
writer.WriteLine(str);
}
}
}
class Program
{
static void Main(string[] args)
{
TcpListener listener = null;
Socket clientSocket = null;
try
{
// 1. 서버 IP 주소 설정
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
// 2. TCPListener 생성 및 시작
// TCPListener: 클라이언트의 연결을 수신
listener = new TcpListener(ipAddress, 5001);
listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작
while (true)
{
// 프로그램이 클라이언트의 연결 요청을 받을 때마다, 새로운 Thread를 생성하고 그 스레드가 클라이언트와의 통신을 처리
// 3. 클라이언트의 연결 대기 및 수락
clientSocket = listener.AcceptSocket();
// 4. 클라이언트 처리기 생성 및 스레드 시작
ClientHandler handler = new ClientHandler(clientSocket);
Thread t = new Thread(new ThreadStart(handler.Chat)); // 새 스레드를 시작하여 클라이언트와의 통신을 독립적으로 처리
t.Start();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
clientSocket.Close();
}
}
}
}