종합 실습 - 비품 대여 관리 콘솔 앱
이 장에서 배우는 것
지금까지 만든 비품 객체와 처리 기능을 하나의 실행 가능한 프로그램으로 묶는다. 작은 사무실에서 노트북과 프로젝터를 빌려주고 돌려받는 상황을 다룬다. 이번에는 사용자가 번호를 입력하는 메뉴를 만들지 않는다. 미리 정한 명령 목록을 처음부터 끝까지 처리한 뒤 실행을 마친다. 같은 명령을 다시 실행했을 때 같은 결과를 얻으므로, 기능이 서로 잘 연결되는지 확인하기 쉽다.
앞 장에서 살펴본 이벤트와 델리게이트는 특정 일이 발생했을 때 연결된 처리를 호출하는 데 쓰였다. 이 프로그램은 정해진 순서로 작업을 호출하므로 이벤트가 필요하지 않다. 배운 기능을 모두 넣는 것이 목적은 아니다. 필요한 기능을 고르고, 각 기능의 책임을 분명하게 나누는 것이 목적이다.
- 정해진 명령 목록으로 대여와 반납을 순서대로 처리한다.
- 이미 대여된 비품과 잘못된 반납 요청을 구별한다.
- 고정된 기준일로 연체 보고를 만들어 실행 결과를 일정하게 유지한다.
- 최종 상태를 파일에 저장하고 업무상 거절과 파일 오류를 구별한다.
- 완성된 코드를 레거시 VB6 코드와 비교하여 실행 환경과 자료형의 차이를 정리한다.
문제 상황
사무실에는 노트북 한 대와 프로젝터 한 대가 있다. 담당자는 비품 번호, 비품 이름, 대여자, 반납 예정일을 기록한다. 비품 번호가 같으면 같은 물건으로 취급한다. 이름이 같더라도 번호가 다르면 별개의 물건이다. 물건의 이름으로 대여 여부를 찾으면 같은 모델의 노트북이 여러 대일 때 혼동할 수 있다.
월요일에 적어 둔 기록을 뒤늦게 입력하는 경우를 생각해 보자. 노트북 대여 기록 다음에 같은 노트북의 대여 요청이 또 들어 있다. 프로젝터에는 정상적인 대여와 반납 기록이 있지만, 반납 기록이 한 번 더 적혀 있다. 존재하지 않는 비품 번호를 사용한 요청도 있다. 프로그램은 각 요청의 결과를 알려 주면서 뒤에 남은 요청을 계속 처리해야 한다.
여기서 실패한 요청을 모두 실행 오류로 취급하면 곤란하다. 이미 대여 중인 비품을 다시 빌려 달라는 요청은 예상 가능한 업무 상황이다. 담당자가 확인할 수 있도록 거절 이유를 출력하면 된다. 반면 저장할 폴더에 쓰기 권한이 없는 상황은 파일 작업의 실패다. 두 상황은 원인이 다르므로 처리 방식도 나누어야 한다.
실습에서는 기준일을 2026년 10월 1일로 고정한다. 노트북의 반납 예정일은 9월 28일이다. 프로젝터는 10월 5일까지 빌렸다가 명령 목록 중간에 반납한다. 마지막 보고에는 노트북만 연체 항목으로 나타나야 한다. 실행할 때마다 현재 날짜를 읽으면 며칠 뒤 같은 코드를 실행한 독자의 결과가 달라지므로, 이번 프로그램에서는 현재 날짜를 읽지 않는다.
| 순서 | 명령 | 확인할 내용 |
|---|---|---|
| 1 | 노트북 대여 | 사용 가능한 비품이 대여 상태로 바뀐다. |
| 2 | 같은 노트북 대여 | 기존 대여 기록을 유지하고 요청을 거절한다. |
| 3~5 | 프로젝터 대여와 두 번의 반납 | 첫 반납만 성공한다. |
| 6 | 없는 비품 대여 | 번호를 찾지 못한 이유를 출력한다. |
| 7~8 | 보고와 저장 | 최종 상태를 화면과 파일에서 확인한다. |
명령 순서와 상태 변경을 분리한다
명령 목록은 이번 실행에서 무엇을 할지 정하는 입력 자료다. 비품 목록은 명령을 처리하면서 바뀌는 업무 자료다. 두 목록을 구별하면 입력 순서를 바꾸어 다른 상황을 시험할 수 있다. 대여 기능 안에 다음 반납 명령을 넣거나, 반납 기능 안에 보고 출력을 넣지 않는다.
명령은 문자열로 기록한다. 예를 들어 대여|NB01|김하나|2026-09-28에서 세로줄은 항목을 나누는 구분자다. 첫 항목은 작업 종류, 두 번째는 비품 번호, 세 번째는 대여자, 네 번째는 반납 예정일이다. 이 형식은 이 실습을 위해 정한 작은 입력 규칙이다. 대여자 이름에 세로줄이 들어가는 경우는 지원하지 않는다.
ProcessCommand는 문자열을 나누고 항목 수와 날짜를 검사한다. 검사가 끝나면 대여, 반납, 보고, 저장 중 하나를 호출한다. 대여와 반납 함수는 명령 문자열의 구분자를 알 필요가 없다. 필요한 비품 번호와 값만 전달받는다. 입력 형식이 바뀌더라도 업무 처리 기능을 그대로 사용할 수 있도록 경계를 둔 것이다.
비품은 Dictionary(Of String, Equipment)에 보관한다. 키는 비품 번호이고 값은 비품 객체다. 번호를 찾을 때는 TryGetValue를 사용한다. 찾았는지를 나타내는 값과 찾은 객체를 함께 얻으므로, 존재하지 않는 번호에 곧바로 접근하지 않아도 된다. 이 프로그램은 번호의 대소문자를 구별하도록 설정한다.
목록의 반복은 마지막 항목을 처리하면 끝난다. 사용자 입력을 기다리는 Console.ReadLine도, 종료 번호를 검사하는 메뉴 반복도 없다. 실행이 끝나는 조건이 명령 목록의 끝으로 정해져 있어 자동으로 결과를 확인하기에 적합하다. 화면에 표시한 결과는 각 명령이 처리된 시점의 결과이며, 마지막 보고는 모든 대여와 반납을 적용한 뒤의 상태다.
대여 상태와 연체 기준을 정한다
비품 객체에는 번호, 이름, 대여자, 반납 예정일을 둔다. 번호와 이름은 생성할 때 정하고 이후에는 읽기만 한다. 대여자는 빈 문자열이면 미대여 상태로 정한다. 반납 예정일은 Nullable(Of Date)로 선언한다. 이는 날짜 값이 있거나 없을 수 있는 형식이다. 미대여 비품의 반납 예정일에는 날짜 대신 Nothing을 넣는다.
이 설계에서는 대여자가 비어 있지 않으면 반납 예정일도 있어야 한다. 대여가 성공할 때 두 값을 함께 설정하고, 반납이 성공할 때 두 값을 함께 비운다. 서로 관련된 값이 맞는 상태를 유지하는 규칙이다. 속성을 외부에서 직접 바꾸는 더 큰 프로그램이라면 이 규칙을 객체 안에서 강제하는 설계도 필요하다. 여기서는 상태 변경을 대여와 반납 함수 두 곳에 모아 흐름을 확인한다.
연체는 대여 중이면서 반납 예정일이 기준일보다 이른 경우로 정의한다. 예정일이 기준일과 같으면 아직 연체가 아니다. 날짜의 시각 부분은 비교하지 않도록 Date 속성으로 날짜 부분만 사용한다. 예정일 다음 날부터 하루 연체가 되며, 9월 28일 예정인 비품은 10월 1일 기준으로 3일 연체다.
보고의 출력 순서는 비품 번호로 정렬한다. 사전에 항목을 넣은 순서에 기대지 않고 출력 기준을 코드로 지정하는 것이다. 날짜도 지역 설정에 맡기지 않고 yyyy-MM-dd 형식으로 표시한다. 날짜 해석에는 CultureInfo.InvariantCulture를 사용한다. 이는 컴퓨터의 언어와 지역 설정에 영향을 받지 않는 문화권 설정이다.
명령에 적힌 날짜는 TryParseExact로 해석한다. 이 함수는 지정한 형식과 맞는 날짜인지 확인하고 성공 여부를 돌려준다. 잘못된 날짜는 명령 거절로 처리한다. 날짜 해석에 실패했는데도 결과 변수를 계속 사용하는 실수를 피하려면, 실패한 자리에서 해당 명령의 처리를 끝내야 한다.
저장 책임과 VB6의 차이를 구별한다
저장 파일에는 실행이 끝난 시점의 비품 상태를 기록한다. 처리한 명령의 이력을 기록하는 파일은 아니다. 한 줄에 비품 하나를 넣고 세로줄로 필드를 나눈다. 프로젝터처럼 반납된 비품은 대여자와 예정일 필드가 빈 상태로 남는다. 번호와 이름은 계속 기록하므로 어떤 비품이 등록되어 있는지 알 수 있다.
파일 경로는 현재 작업 폴더의 equipment-state.txt다. 보통 프로젝트 폴더에서 dotnet run을 실행하면 그 폴더에 저장된다. 같은 이름의 파일이 있으면 새 최종 상태로 덮어쓴다. 실행 결과를 확인할 때는 실습용 폴더를 사용한다. 이번 코드에는 저장 파일을 다시 읽어 상태를 복원하는 기능이 없다. 새로 실행하면 초기 비품 두 개부터 다시 명령을 처리한다.
저장에는 유니코드 문자 인코딩 방식인 UTF-8을 사용한다. 파일 앞에 붙는 인코딩 표시인 바이트 순서 표식(BOM)은 넣지 않는다. 줄바꿈은 코드에서 vbLf로 지정한다. 화면 출력의 실제 줄바꿈 바이트는 운영체제에 따르지만, 저장 파일의 내용은 같은 명령과 자료로 실행하면 같게 만든다.
| 항목 | 이번 프로그램 | VB6 코드에서 주의할 점 |
|---|---|---|
| 실행 기반 | macOS와 Linux에서 .NET 콘솔 앱으로 실행한다. | 전통적인 VB6 앱은 Windows 실행 환경을 전제로 한다. |
| 정수 형식 | Integer는 32비트, Long은 64비트다. | Integer는 16비트, Long은 32비트다. |
| 오류 처리 | 파일 작업을 Try와 Catch로 감싼다. | On Error 기반의 오류 처리 흐름을 만날 수 있다. |
| 자료 보관 | 형식을 지정한 Dictionary를 사용한다. | Variant나 형식이 다른 항목을 담은 Collection의 사용을 확인해야 한다. |
문법이 비슷하다고 VB6 코드를 그대로 붙여 넣을 수 있는 것은 아니다. 예를 들어 정수 형식의 크기가 달라지면 저장한 자료나 외부 기능과 주고받는 값의 해석도 확인해야 한다. 또한 Option Strict On은 이 .NET 코드에서 암시적인 축소 변환과 늦은 바인딩을 제한한다. 늦은 바인딩은 객체의 실제 형식을 실행 중에 확인해 멤버를 호출하는 방식이다. 문자열 날짜를 날짜 변수에 바로 넣는 식의 코드는 명시적인 해석 과정으로 바꾼다.
비품 번호 같은 문자열에 Nothing을 넣는 것과, 값이 없을 수 있는 날짜에 Nothing을 넣는 것도 구별해야 한다. 일반적인 Date 변수에 Nothing을 대입하면 날짜가 없는 상태를 표현하는 것이 아니라 그 형식의 기본값이 된다. 이번 코드가 Nullable(Of Date)를 쓰는 이유다.
관련 사실을 더 확인하려면 Option Strict 설명, 값이 없을 수 있는 값 형식 설명, File.WriteAllText 설명을 참고할 수 있다.
완성 코드
dotnet new console -lang VB로 만든 프로젝트의 Program.vb 내용을 아래 코드로 바꾼다. 추가 패키지는 필요하지 않다. 명령 문자열을 제외한 상태 변경은 함수에 모았으며, 모든 기능은 한 파일에 들어 있다.
Option Strict On
Option Explicit On
Imports System
Imports System.Collections.Generic
Imports System.Globalization
Imports System.IO
Imports System.Linq
Imports System.Text
Module Program
Private ReadOnly ReferenceDay As Date = New Date(2026, 10, 1)
Private Const SavePath As String = "equipment-state.txt"
Sub Main()
Dim items As New Dictionary(Of String, Equipment)(
StringComparer.Ordinal) From {
{"NB01", New Equipment("NB01", "노트북")},
{"PJ01", New Equipment("PJ01", "프로젝터")}
}
Dim commands As String() = {
"대여|NB01|김하나|2026-09-28",
"대여|NB01|박준|2026-10-03",
"대여|PJ01|이서연|2026-10-05",
"반납|PJ01",
"반납|PJ01",
"대여|XX01|최민|2026-10-04",
"보고",
"저장"
}
Console.WriteLine("비품 대여 관리")
Console.WriteLine("기준일: " & FormatDay(ReferenceDay))
For Each command As String In commands
ProcessCommand(command, items)
Next
Console.WriteLine("처리 종료")
End Sub
Private Sub ProcessCommand(
command As String,
items As Dictionary(Of String, Equipment))
Dim parts As String() = command.Split("|"c)
Select Case parts(0)
Case "대여"
If parts.Length <> 4 Then
Console.WriteLine("명령 거절: 대여 항목 수 오류")
Return
End If
Dim dueDate As Date
If Not Date.TryParseExact(
parts(3), "yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None, dueDate) Then
Console.WriteLine("명령 거절: 날짜 형식 오류")
Return
End If
Console.WriteLine(
RentItem(items, parts(1), parts(2), dueDate))
Case "반납"
If parts.Length <> 2 Then
Console.WriteLine("명령 거절: 반납 항목 수 오류")
Return
End If
Console.WriteLine(ReturnItem(items, parts(1)))
Case "보고"
If parts.Length <> 1 Then
Console.WriteLine("명령 거절: 보고 항목 수 오류")
Return
End If
PrintReport(items)
Case "저장"
If parts.Length <> 1 Then
Console.WriteLine("명령 거절: 저장 항목 수 오류")
Return
End If
SaveState(items)
Case Else
Console.WriteLine("명령 거절: 알 수 없는 작업")
End Select
End Sub
Private Function RentItem(
items As Dictionary(Of String, Equipment),
itemId As String,
borrower As String,
dueDate As Date) As String
Dim item As Equipment = Nothing
If Not items.TryGetValue(itemId, item) Then
Return "대여 거절: " & itemId & " 비품 없음"
End If
If item.IsOnLoan Then
Return "대여 거절: " & itemId & " 이미 대여 중"
End If
If String.IsNullOrWhiteSpace(borrower) Then
Return "대여 거절: 대여자 이름 없음"
End If
item.Borrower = borrower.Trim()
item.DueDate = dueDate.Date
Return "대여 완료: " & item.Id & " / " &
item.Borrower & " / 예정일 " & FormatDay(dueDate)
End Function
Private Function ReturnItem(
items As Dictionary(Of String, Equipment),
itemId As String) As String
Dim item As Equipment = Nothing
If Not items.TryGetValue(itemId, item) Then
Return "반납 거절: " & itemId & " 비품 없음"
End If
If Not item.IsOnLoan Then
Return "반납 거절: " & itemId & " 대여 기록 없음"
End If
item.Borrower = String.Empty
item.DueDate = Nothing
Return "반납 완료: " & item.Id
End Function
Private Sub PrintReport(
items As Dictionary(Of String, Equipment))
Console.WriteLine("[현재 상태]")
For Each item As Equipment In items.Values.OrderBy(
Function(value) value.Id, StringComparer.Ordinal)
If item.IsOnLoan Then
Console.WriteLine(
item.Id & " " & item.Name & ": 대여 중 / " &
item.Borrower & " / " & FormatDay(item.DueDate.Value))
Else
Console.WriteLine(
item.Id & " " & item.Name & ": 대여 가능")
End If
Next
Console.WriteLine("[연체 보고]")
Dim overdueItems = From item In items.Values
Where item.IsOnLoan AndAlso
item.DueDate.HasValue AndAlso
item.DueDate.Value.Date < ReferenceDay.Date
Order By item.Id
Select item
Dim count As Integer = 0
For Each item As Equipment In overdueItems
Dim days As Integer =
(ReferenceDay.Date - item.DueDate.Value.Date).Days
Console.WriteLine(
item.Id & " " & item.Name & " / " &
item.Borrower & " / " &
days.ToString(CultureInfo.InvariantCulture) & "일 연체")
count += 1
Next
Console.WriteLine(
"연체 건수: " & count.ToString(CultureInfo.InvariantCulture))
End Sub
Private Sub SaveState(
items As Dictionary(Of String, Equipment))
Dim lines As New List(Of String) From {
"번호|이름|대여자|반납예정일"
}
For Each item As Equipment In items.Values.OrderBy(
Function(value) value.Id, StringComparer.Ordinal)
Dim dueText As String = String.Empty
If item.DueDate.HasValue Then
dueText = FormatDay(item.DueDate.Value)
End If
lines.Add(
item.Id & "|" & item.Name & "|" &
item.Borrower & "|" & dueText)
Next
Try
Dim content As String = String.Join(vbLf, lines) & vbLf
File.WriteAllText(SavePath, content, New UTF8Encoding(False))
Console.WriteLine("저장 완료: " & SavePath)
Catch ex As IOException
Console.WriteLine("저장 실패: 파일 입출력 오류")
Catch ex As UnauthorizedAccessException
Console.WriteLine("저장 실패: 쓰기 권한 없음")
End Try
End Sub
Private Function FormatDay(value As Date) As String
Return value.ToString(
"yyyy-MM-dd", CultureInfo.InvariantCulture)
End Function
End Module
Public Class Equipment
Public ReadOnly Property Id As String
Public ReadOnly Property Name As String
Public Property Borrower As String = String.Empty
Public Property DueDate As Nullable(Of Date)
Public ReadOnly Property IsOnLoan As Boolean
Get
Return Borrower.Length > 0
End Get
End Property
Public Sub New(itemId As String, itemName As String)
Id = itemId
Name = itemName
End Sub
End Class
줄별 해설
맨 앞의 Option Strict On은 형식을 느슨하게 섞는 코드를 제한한다. Option Explicit On은 변수를 선언한 뒤 사용하도록 요구한다. 이 옵션들은 파일의 맨 위에 둔다. 이어지는 Imports는 사전, 날짜 형식, 파일, 쿼리, 인코딩에 필요한 이름을 짧게 쓸 수 있게 한다.
ReferenceDay는 보고의 기준일이고 SavePath는 저장 파일 이름이다. 여러 함수가 같은 값을 사용하므로 모듈에 둔다. 기준일은 읽기 전용으로 선언해 처리 중에 바뀌지 않게 한다. 저장 경로는 문자열 상수다. 출력되는 날짜와 파일 이름을 확인할 때 이 두 줄이 출발점이 된다.
Main에서 사전을 만들면서 비품 객체 두 개를 등록한다. 사전의 키와 객체의 Id에는 같은 번호를 넣는다. 다른 값을 넣으면 검색에 사용하는 번호와 출력에 표시하는 번호가 달라지므로, 등록할 때 둘을 맞춘다. StringComparer.Ordinal은 문자열을 문화권의 정렬 관습이 아닌 정해진 문자 비교 기준으로 비교한다.
명령 배열은 업무 사례 자체를 표현한다. 첫 번째 명령이 노트북을 빌려주므로 두 번째 명령은 거절되어야 한다. 세 번째부터 다섯 번째까지는 프로젝터의 상태를 대여 중, 대여 가능, 대여 가능으로 바꾼다. 두 번째 반납이 거절되어도 반복은 계속된다. 거절은 함수가 돌려준 메시지이지 반복을 중단하라는 지시가 아니다.
ProcessCommand의 Split("|"c)에서 c는 한 글자 문자 값임을 나타낸다. 나눈 결과의 첫 항목으로 작업을 고르고, 각 작업이 요구하는 항목 수를 확인한다. 항목 수 검사를 먼저 해야 parts(3)처럼 뒤쪽 항목을 안전하게 읽을 수 있다. 빈 명령 문자열도 첫 항목이 빈 문자열이므로 알 수 없는 작업으로 처리된다.
날짜를 읽는 부분은 TryParseExact가 실패하면 메시지를 출력하고 Return으로 현재 프로시저를 끝낸다. 여기서 끝나는 것은 해당 명령의 처리다. 호출한 Main의 반복은 다음 명령으로 이어진다. 코드가 어떤 범위를 끝내는지 구별하면 실패 처리 뒤의 실행 흐름을 이해하기 쉽다.
RentItem은 비품 존재 여부, 기존 대여 여부, 대여자 이름 순으로 검사한다. 검사에 실패한 경우에는 객체의 속성을 바꾸기 전에 반환한다. 따라서 박준의 대여 요청이 거절되어도 노트북의 대여자는 김하나로 남는다. 모든 검사를 통과한 뒤에만 대여자와 날짜를 함께 기록한다.
ReturnItem도 먼저 비품을 찾는다. 미대여 상태라면 빈 대여 정보를 다시 쓰지 않고 거절 메시지를 반환한다. 정상 반납은 대여자 문자열과 예정일을 모두 비운다. 이때 비품 객체 자체를 사전에서 지우지 않는다. 비품 등록과 대여 상태는 서로 다른 정보이기 때문이다.
PrintReport의 첫 반복은 모든 비품을 번호순으로 보여 준다. 이어지는 쿼리는 연체 조건을 만족하는 객체만 고른다. AndAlso는 앞 조건이 거짓이면 뒤 조건을 평가하지 않는다. 예정일이 있는지 확인한 다음 Value로 날짜를 읽는 순서에 의미가 있다. 이 쿼리는 만들어진 자리에서 결과를 모두 확정하는 것이 아니라 반복할 때 조건을 평가한다. 코드에서는 곧바로 반복하므로 그 사이에 비품 상태가 바뀌지 않는다.
연체 일수는 기준일에서 예정일을 뺀 시간 간격의 Days로 얻는다. 양쪽 날짜를 날짜 부분으로 맞췄으므로 시각 차이 때문에 하루가 줄어드는 일이 없다. 연체 건수는 실제 출력한 항목마다 한 번씩 늘린다. 연체 비품이 없다면 항목 줄 없이 연체 건수 0만 출력한다.
SaveState는 먼저 메모리에서 저장할 줄을 만든다. 대여 중이 아닌 비품에도 번호와 이름을 기록하고, 날짜가 없으면 빈 필드를 넣는다. 그 뒤 파일 쓰기만 Try 안에서 실행한다. 쓰기에 성공한 다음에 성공 메시지를 출력하므로, 저장이 실패했는데 완료했다고 알리는 순서를 피한다.
마지막의 Equipment 클래스는 비품 한 개의 자료를 담는다. IsOnLoan은 별도 변수를 저장하지 않고 대여자 문자열에서 계산한다. 대여자와 대여 여부를 각각 저장하면 둘을 맞춰 갱신해야 하지만, 여기서는 대여 여부가 대여자 값에서 바로 결정된다. 이 설계가 성립하도록 대여 함수는 공백뿐인 이름을 거절한다.
실행 결과
빈 실습 폴더에서 다음 명령으로 프로젝트를 만든다. 생성된 Program.vb를 완성 코드로 교체한 뒤 실행한다. .NET 10 SDK가 설치되어 있고 현재 폴더에 쓰기 권한이 있는 환경을 기준으로 한다.
dotnet new console -lang VB
dotnet run
프로그램의 예상 출력은 다음과 같다. 프로젝트 생성 명령의 안내 문구는 프로그램 출력에 포함하지 않는다.
비품 대여 관리
기준일: 2026-10-01
대여 완료: NB01 / 김하나 / 예정일 2026-09-28
대여 거절: NB01 이미 대여 중
대여 완료: PJ01 / 이서연 / 예정일 2026-10-05
반납 완료: PJ01
반납 거절: PJ01 대여 기록 없음
대여 거절: XX01 비품 없음
[현재 상태]
NB01 노트북: 대여 중 / 김하나 / 2026-09-28
PJ01 프로젝터: 대여 가능
[연체 보고]
NB01 노트북 / 김하나 / 3일 연체
연체 건수: 1
저장 완료: equipment-state.txt
처리 종료
macOS와 Linux에서는 다음 명령으로 저장 파일을 확인한다.
cat equipment-state.txt
번호|이름|대여자|반납예정일
NB01|노트북|김하나|2026-09-28
PJ01|프로젝터||
프로젝터 줄의 마지막 세로줄 두 개는 대여자와 반납 예정일이 모두 비어 있음을 나타낸다. 저장 파일은 제목 줄과 비품 두 줄로 구성되며 마지막 줄 뒤에도 줄바꿈이 있다. 다시 dotnet run을 실행하면 초기 객체를 새로 만들고 같은 명령을 적용하므로 같은 상태 파일을 덮어쓴다.
이 출력만으로 파일 쓰기 실패 상황까지 확인한 것은 아니다. 쓰기 권한이 없거나 파일 시스템에 문제가 있으면 저장 완료 대신 해당 실패 메시지가 나온다. 그 경우에도 파일 오류를 처리한 뒤 처리 종료를 출력한다. 정상 실행의 출력과 실패 환경의 출력을 구별해서 읽어야 한다.
실무에서 자주 틀리는 것
대여 가능 여부를 확인하기 전에 기록을 덮어쓴다
다음 코드는 새 이름을 먼저 기록한다. 그러면 원래 대여자를 잃고, 새 이름 때문에 대여 중으로 판단한다. 거절 메시지를 내더라도 자료는 이미 바뀐 상태다.
item.Borrower = borrower
If item.IsOnLoan Then
Return "대여 거절"
End If
검사를 먼저 하고, 성공하는 경로에서만 값을 변경한다. 아래는 대여 함수 안에 들어갈 수정된 순서다.
If item.IsOnLoan Then
Return "대여 거절"
End If
item.Borrower = borrower.Trim()
item.DueDate = dueDate.Date
기준일과 같은 예정일도 연체로 센다
다음 조건은 예정일 당일을 연체로 분류한다. 이 실습의 규칙은 예정일이 지난 뒤부터 연체이므로 등호가 들어가면 결과가 달라진다.
Dim isOverdue As Boolean =
item.DueDate.HasValue AndAlso
item.DueDate.Value.Date <= ReferenceDay.Date
수정한 조건에서는 날짜가 더 이른 경우만 고른다. 전체 보고에서는 대여 중인지도 함께 검사한다.
Dim isOverdue As Boolean =
item.IsOnLoan AndAlso
item.DueDate.HasValue AndAlso
item.DueDate.Value.Date < ReferenceDay.Date
반납하면서 대여자만 비운다
대여자를 지우면 화면에서는 대여 가능으로 보이지만, 예정일이 남아 저장 파일에 오래된 날짜가 들어간다. 화면과 파일이 서로 다른 뜻의 정보를 전달하게 된다.
item.Borrower = String.Empty
Return "반납 완료"
반납 성공 경로에서 예정일까지 함께 비운다. 대여와 반납이 한 쌍의 값을 함께 관리한다는 규칙을 유지한다.
item.Borrower = String.Empty
item.DueDate = Nothing
Return "반납 완료"
저장하기 전에 완료 메시지를 출력한다
아래 코드는 파일 쓰기가 실패해도 이미 저장 완료를 화면에 표시한 뒤다. 사용자가 메시지만 보고 저장되었다고 판단할 수 있다.
Console.WriteLine("저장 완료: " & SavePath)
File.WriteAllText(SavePath, content, New UTF8Encoding(False))
성공 메시지는 파일 쓰기가 반환된 뒤에 출력한다. 실제 프로그램에서는 완성 코드처럼 이 부분을 파일 오류 처리 안에 둔다.
File.WriteAllText(SavePath, content, New UTF8Encoding(False))
Console.WriteLine("저장 완료: " & SavePath)
한눈에 보기
| 기능 | 담당 코드 | 확인 기준 |
|---|---|---|
| 실행 순서 | Main의 명령 배열과 반복 | 마지막 명령 뒤에 실행이 끝난다. |
| 명령 검사 | ProcessCommand | 항목 수와 날짜를 확인한 뒤 업무 함수를 호출한다. |
| 대여와 반납 | RentItem, ReturnItem | 거절된 요청은 기존 상태를 바꾸지 않는다. |
| 연체 보고 | PrintReport | 대여 중이며 예정일이 기준일보다 이르다. |
| 파일 저장 | SaveState | 번호순으로 최종 상태를 쓰고 성공 여부를 알린다. |
| 날짜 표시 | FormatDay | 지역 설정과 관계없이 같은 형식으로 표시한다. |
완성 코드를 읽을 때는 입력, 상태 변경, 결과 출력의 순서로 따라가면 된다. 같은 문자열을 다시 출력하는 것보다 중요한 확인 사항은 거절된 요청이 자료를 그대로 유지하는지, 반납 후 두 값이 함께 비워지는지, 보고와 파일이 같은 최종 상태를 보여 주는지다.
연습 문제
- 명령 배열의 첫 번째 대여 예정일을
2026-10-01로 바꾼다. 현재 상태와 연체 보고에서 무엇이 달라지는지 설명한다. 보고명령 바로 앞에반납|NB01을 추가한다. 현재 상태, 연체 건수, 저장 파일의 노트북 줄을 예상한다.- 명령 배열의 맨 앞에
대여|NB01|김하나|2026-02-30을 추가한다. 이 명령이 비품 상태를 바꾸지 않는 이유와 다음 명령의 처리 결과를 설명한다. - 대여자가 같은 시점에 비품을 하나만 빌릴 수 있도록
RentItem을 수정한다. 같은 이름 비교는 대소문자를 구별하는 문자 비교로 정한다. 기존 비품 존재 여부와 대여 여부 검사는 유지한다.
정답과 해설
-
노트북의 현재 상태에 표시되는 예정일은
2026-10-01이 된다. 노트북은 여전히 대여 중이지만 예정일이 기준일과 같으므로 연체 항목은 나오지 않고연체 건수: 0이 출력된다. 저장 파일의 노트북 예정일도 바뀐다. 대여 상태와 연체 상태는 같은 뜻이 아니다. -
노트북 반납이 성공한다. 보고에서 노트북과 프로젝터가 모두 대여 가능으로 표시되고, 연체 건수는 0이다. 저장 파일의 노트북 줄은 다음과 같다. 비품을 반납했으므로 대여자와 예정일 필드가 함께 비어 있다.
NB01|노트북|| -
2월 30일은 존재하지 않는 날짜이므로
TryParseExact가 실패한다.명령 거절: 날짜 형식 오류를 출력하고RentItem을 호출하지 않는다. 따라서 노트북은 미대여 상태로 남는다. 그다음 원래 첫 번째 명령은 정상적으로 실행되어 김하나에게 노트북을 대여한다. -
공백 이름 검사 뒤, 속성을 변경하기 전에 아래 코드를 넣는다. 먼저 앞뒤 공백을 제거한 이름을 만들고, 이미 대여 중인 비품 중 같은 이름이 있는지 확인한다.
Any는 조건을 만족하는 항목이 하나라도 있으면 참을 돌려준다.Dim normalizedName As String = borrower.Trim() Dim alreadyBorrowing As Boolean = items.Values.Any( Function(value) value.IsOnLoan AndAlso String.Equals( value.Borrower, normalizedName, StringComparison.Ordinal)) If alreadyBorrowing Then Return "대여 거절: 대여자에게 기존 대여 있음" End If item.Borrower = normalizedName item.DueDate = dueDate.Date위 코드의 마지막 두 줄은 기존 속성 대입 두 줄을 대신한다. 확인하려면 프로젝터 대여 명령의 이름을 김하나로 바꾼다. 노트북을 먼저 빌린 상태이므로 프로젝터 대여는 거절된다. 뒤의 프로젝터 반납 두 건도 대여 기록이 없어 거절된다. 실제 사무실에서는 동명이인이 있을 수 있으므로 이름 대신 대여자 번호를 사용하는 설계가 필요하다.
이번 프로그램은 명령 목록을 처리하고 결과를 저장한 뒤 끝난다. 기능을 늘릴 때도 입력 검사, 업무 규칙, 저장의 책임을 구별하는 구조는 유지할 수 있다. 먼저 바꾸려는 규칙을 한 문장으로 정하고, 그 규칙을 확인할 명령을 추가한 다음 결과를 비교한다. 비품 한 개의 상태에서 시작해 여러 요청의 결과까지 설명할 수 있다면, 이 책에서 만든 프로그램의 흐름을 스스로 읽고 수정할 준비가 된 것이다.