작성자: 스콧 미첼
이 자습서에서는 ASP.NET 데이터 웹 컨트롤의 삽입, 업데이트 또는 삭제 작업 중에 예외가 발생할 경우 친숙한 정보 오류 메시지를 표시하는 방법을 알아보세요.
소개
계층화된 애플리케이션 아키텍처를 사용하여 ASP.NET 웹 애플리케이션의 데이터를 사용하는 경우 다음 세 가지 일반적인 단계가 포함됩니다.
- 호출해야 하는 비즈니스 논리 계층의 메서드와 이를 전달할 매개 변수 값을 결정합니다. 매개 변수 값은 하드 코딩, 프로그래밍 방식으로 할당되거나 사용자가 입력한 입력일 수 있습니다.
- 메서드를 호출합니다.
- 결과를 처리합니다. 데이터를 반환하는 BLL 메서드를 호출할 때 데이터 웹 컨트롤에 데이터를 바인딩하는 작업이 포함될 수 있습니다. 데이터를 수정하는 BLL 메서드의 경우 반환 값에 따라 일부 작업을 수행하거나 2단계에서 발생한 예외를 정상적으로 처리하는 작업이 포함될 수 있습니다.
이전 자습서에서 보았듯이 ObjectDataSource와 데이터 웹 컨트롤은 모두 1단계와 3단계의 확장성 지점을 제공합니다. 예를 들어 GridView는 RowUpdating
이벤트를 발생시키고, 해당 필드 값을 ObjectDataSource의 UpdateParameters
컬렉션에 할당합니다. RowUpdated
이벤트는 ObjectDataSource가 작업을 완료한 후에 발생합니다.
1단계 중에 발생하는 이벤트를 이미 검사했으며 입력 매개 변수를 사용자 지정하거나 작업을 취소하는 데 사용할 수 있는 방법을 살펴보았습니다. 이 자습서에서는 작업이 완료된 후 발생하는 이벤트에 주의를 기울입니다. 이러한 사후 수준 이벤트 처리기를 사용하여 작업 중에 예외가 발생했는지 확인하고 정상적으로 처리하여 표준 ASP.NET 예외 페이지로 기본 설정하지 않고 화면에 친숙한 정보 오류 메시지를 표시할 수 있습니다.
이러한 사후 수준 이벤트 작업을 설명하기 위해 편집 가능한 GridView에서 제품을 나열하는 페이지를 만들어 보겠습니다. 제품을 업데이트할 때 예외가 발생하면 ASP.NET 페이지에 문제가 발생했음을 설명하는 짧은 메시지가 GridView 위에 표시됩니다. 시작해 봅시다!
1단계: 제품의 편집 가능한 GridView 만들기
이전 자습서에서는 두 개의 필드, ProductName
및 UnitPrice
가 있는 편집 가능한 GridView를 만들었습니다. 이렇게 하려면 각 제품 필드에 대한 ProductsBLL
매개 변수가 아닌 세 개의 입력 매개 변수(제품 이름, 단가 및 ID)만 허용하는 클래스 UpdateProduct
메서드에 대한 추가 오버로드를 만들어야 했습니다. 이 자습서에서는 제품의 이름, 단위당 수량, 단가 및 재고 단위를 표시하지만 재고의 이름, 단가 및 단위만 편집할 수 있는 편집 가능한 GridView를 만들어 이 기술을 다시 연습해 보겠습니다.
이 시나리오를 수용하려면 제품의 이름, 단가, 재고 단위 및 ID의 네 가지 매개 변수를 허용하는 메서드의 또 다른 오버로드 UpdateProduct
가 필요합니다.
ProductsBLL
클래스에 다음 메서드를 추가합니다.
[System.ComponentModel.DataObjectMethodAttribute(
System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock,
int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
product.ProductName = productName;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
이 메서드가 완료되면 이러한 네 가지 특정 제품 필드를 편집할 수 있는 ASP.NET 페이지를 만들 준비가 완료되었습니다. 폴더에서 ErrorHandling.aspx
EditInsertDelete
페이지를 열고 디자이너를 통해 페이지에 GridView를 추가합니다. GridView를 새 ObjectDataSource에 바인딩하고, Select()
메서드를 ProductsBLL
클래스의 GetProducts()
메서드에 매핑하고, Update()
메서드를 방금 만든 UpdateProduct
오버로드에 매핑합니다.
그림 1: 4개의 입력 매개 변수를 허용하는 메서드 오버로드 사용 UpdateProduct
(전체 크기 이미지를 보려면 클릭)
이렇게 하면 4개의 매개 변수가 UpdateParameters
있는 컬렉션과 각 제품 필드에 대한 필드가 있는 GridView가 있는 ObjectDataSource가 만들어집니다. ObjectDataSource의 선언적 태그는 OldValuesParameterFormatString
속성에 값을 original_{0}
로 할당합니다. 하지만 BLL 클래스는 이름이 original_productID
인 입력 매개변수가 전달되는 것을 예상하지 않으므로 예외가 발생할 수 있습니다. 선언적 구문에서 이 설정을 완전히 제거하거나 기본값 {0}
으로 설정해야 합니다.
다음으로, GridView를 제한하여 ProductName
, QuantityPerUnit
, UnitPrice
, UnitsInStock
BoundFields만 포함합니다. 또한 필요하다고 판단되는 경우, 필드 수준의 서식(예: HeaderText
속성 변경)을 자유롭게 적용할 수 있습니다.
이전 자습서에서는 읽기 전용 모드와 편집 모드 모두에서 BoundField의 UnitPrice
형식을 통화로 지정하는 방법을 살펴보았습니다. 여기서도 동일한 작업을 수행해 보겠습니다. 그림 2에 표시된 것처럼 BoundField의 DataFormatString
속성을 {0:c}
, HtmlEncode
속성을 false
, 그리고 ApplyFormatInEditMode
속성을 true
로 설정했습니다.
그림 2: 통화로 표시하도록 BoundField 구성 UnitPrice
(전체 크기 이미지를 보려면 클릭)
편집 인터페이스에서 UnitPrice
를 통화로 형식 지정하려면, 통화로 형식화된 문자열을 RowUpdating
값으로 구문 분석할 수 있는 GridView의 decimal
이벤트에 대한 이벤트 처리기를 만들어야 합니다. 이전 자습서의 RowUpdating
이벤트 처리기는 사용자가 UnitPrice
값을 제공했는지 확인하기 위해 체크했습니다. 그러나 이 자습서에서는 사용자가 가격을 생략하도록 허용해 보겠습니다.
protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
if (e.NewValues["UnitPrice"] != null)
e.NewValues["UnitPrice"] =decimal.Parse(e.NewValues["UnitPrice"].ToString(),
System.Globalization.NumberStyles.Currency);
}
GridView에는 BoundField가 QuantityPerUnit
포함되어 있지만 이 BoundField는 표시 용도로만 사용해야 하며 사용자가 편집할 수 없어야 합니다. 이를 정렬하려면 BoundFields의 ReadOnly
속성을 .로 설정하기만 하면 됩니다 true
.
그림 3: BoundField Read-Only 만들기 QuantityPerUnit
(전체 크기 이미지를 보려면 클릭)
마지막으로 GridView의 스마트 태그에서 편집 사용 확인란을 선택합니다. 이러한 단계를 완료한 후 페이지의 디자이너는 ErrorHandling.aspx
그림 4와 유사해야 합니다.
그림 4: 필요한 BoundFields를 제외한 모든 항목을 제거하고 편집 사용 확인란을 선택합니다(전체 크기 이미지를 보려면 클릭).
이 시점에서 우리는 모든 제품의 ProductName
, QuantityPerUnit
, UnitPrice
, UnitsInStock
필드를 목록화했지만, 편집할 수 있는 필드는 ProductName
, UnitPrice
, UnitsInStock
뿐입니다.
그림 5: 이제 사용자가 재고 필드에서 제품의 이름, 가격 및 단위를 쉽게 편집할 수 있습니다(전체 크기 이미지를 보려면 클릭).
2단계: DAL-Level 예외를 정상적으로 처리
편집 가능한 GridView는 사용자가 편집된 제품의 이름, 가격 및 재고 단위에 대한 법적 값을 입력할 때 훌륭하게 작동하지만 잘못된 값을 입력하면 예외가 발생합니다. 예를 들어 ProductName
값을 생략하면, 클래스의 속성이 ProductName
로 설정되어 있기 때문에 ProductsRow
이 발생합니다. 데이터베이스가 다운되어 있을 때 데이터베이스에 연결을 시도하면 TableAdapter에서 AllowDBNull
예외가 발생합니다. 이러한 예외는 아무 작업도 수행하지 않고 데이터 액세스 계층에서 비즈니스 논리 계층으로, ASP.NET 페이지로, 마지막으로 ASP.NET 런타임으로 버블업됩니다.
웹 애플리케이션을 구성하는 방법 및 애플리케이션 localhost
을 방문하는지 여부에 따라 처리되지 않은 예외로 인해 일반 서버 오류 페이지, 자세한 오류 보고서 또는 사용자에게 친숙한 웹 페이지가 발생할 수 있습니다. ASP.NET 런타임이 catch되지 않은 예외에 응답하는 방법에 대한 자세한 내용은 ASP.NET 웹 애플리케이션 오류 처리 및 customErrors 요소를 참조하세요.
그림 6은 값을 지정 ProductName
하지 않고 제품을 업데이트하려고 할 때 발생하는 화면을 보여줍니다. 이 보고서는 통과 시 표시되는 localhost
기본 세부 오류 보고서입니다.
그림 6: 제품 이름을 생략하면 예외 세부 정보가 표시됩니다(전체 크기 이미지를 보려면 클릭).
이러한 예외 세부 정보는 애플리케이션을 테스트할 때 유용하지만 예외에 직면하여 최종 사용자에게 이러한 화면을 표시하는 것은 이상적이지 않습니다. 일반 사용자는 NoNullAllowedException
가 무엇인지 또는 왜 발생했는지를 알지 못할 가능성이 높습니다. 더 나은 방법은 사용자에게 제품 업데이트를 시도하는 데 문제가 있음을 설명하는 보다 친숙한 메시지를 표시하는 것입니다.
작업을 수행할 때 예외가 발생하는 경우 ObjectDataSource와 데이터 웹 컨트롤의 사후 수준 이벤트는 이를 감지하고 ASP.NET 런타임까지 버블링에서 예외를 취소하는 수단을 제공합니다. 이 예제에서는 예외가 발생했는지 여부와 예외가 발생한 경우 레이블 웹 컨트롤에 예외 세부 정보를 표시하는 GridView RowUpdated
이벤트에 대한 이벤트 처리기를 만들어 보겠습니다.
먼저 ASP.NET 페이지에 레이블을 추가하고, ID
속성을 ExceptionDetails
로 설정한 다음, Text
속성을 지웁니다. 이 메시지에 사용자의 눈을 끌려면 해당 CssClass
속성을 Warning
로 설정합니다. 이는 이전 자습서에서 Styles.css
파일에 추가한 CSS 클래스입니다. 이 CSS 클래스를 사용하면 레이블의 텍스트가 빨간색, 기울기, 굵게, 매우 큰 글꼴로 표시됩니다.
그림 7: 페이지에 레이블 웹 컨트롤 추가(전체 크기 이미지를 보려면 클릭)
예외가 발생한 직후에만 이 레이블 웹 컨트롤을 표시하도록 하려면 이벤트 처리기에서 Visible
해당 Page_Load
속성을 false로 설정합니다.
protected void Page_Load(object sender, EventArgs e)
{
ExceptionDetails.Visible = false;
}
이 코드를 사용하면 첫 번째 페이지 방문 및 후속 포스트백에서 컨트롤의 ExceptionDetails
속성이 Visible
.로 false
설정됩니다. GridView의 RowUpdated
이벤트 처리기에서 감지할 수 있는 DAL 또는 BLL 수준 예외에 직면할 경우, ExceptionDetails
컨트롤의 Visible
속성을 true로 설정합니다. 웹 제어 이벤트 처리기는 페이지 수명 주기에서 Page_Load
이벤트 처리기 후에 발생하므로 레이블이 표시됩니다. 그러나 다음 Page_Load
포스트백에서 이벤트 처리기는 Visible
속성을 false
으로 다시 되돌려서 다시 보이지 않게 숨깁니다.
비고
또는 선언적 구문에서 ExceptionDetails
속성을 Visible
으로 할당하고 Page_Load
속성을 Visible
으로 설정하여 뷰 상태를 비활성화함으로써 false
의 EnableViewState
속성을 false
에서 설정해야 하는 필요성을 제거할 수 있습니다. 향후 자습서에서는 이 대체 방법을 사용할 것입니다.
레이블 컨트롤이 추가되면 다음 단계는 GridView 이벤트에 RowUpdated
대한 이벤트 처리기를 만드는 것입니다. 디자이너에서 GridView를 선택하고 속성 창으로 이동한 다음 번개 모양 아이콘을 클릭하여 GridView의 이벤트를 나열합니다. 이 자습서의 앞부분에서 RowUpdating
이벤트에 대한 이벤트 처리기를 만들었으므로 RowUpdating
이벤트에 대한 GridView의 항목이 이미 있어야 합니다.
RowUpdated
이벤트에 대한 이벤트 처리기를 또한 만듭니다.
그림 8: GridView의 RowUpdated
이벤트에 대한 이벤트 처리기 만들기
비고
코드 숨김 클래스 파일 상단의 드롭다운 목록을 통해 이벤트 처리기를 만들 수도 있습니다. 왼쪽의 드롭다운 목록에서 GridView를 RowUpdated
선택하고 오른쪽의 드롭다운 목록에서 이벤트를 선택합니다.
이 이벤트 처리기를 만들면 ASP.NET 페이지의 코드 숨김 클래스에 다음 코드가 추가됩니다.
protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
}
이 이벤트 처리기의 두 번째 입력 매개 변수는 예외 처리에 대한 세 가지 속성이 있는 GridViewUpdatedEventArgs 형식의 개체입니다.
-
Exception
throw된 예외에 대한 참조입니다. 예외가 throw되지 않은 경우 이 속성의 값은null
-
ExceptionHandled
는RowUpdated
이벤트 처리기에서 예외가 처리되었는지 여부를 나타내는 부울 값입니다. 만약false
(기본값)인 경우, 예외가 다시 throw되어 ASP.NET 런타임에 전달됩니다. -
KeepInEditMode
이true
로 설정된 경우, 편집된 GridView 행은 편집 모드로 유지됩니다.false
(기본값)로 설정 시, GridView 행은 읽기 전용 모드로 되돌아갑니다.
그렇다면 우리의 코드는 Exception
가 null
가 아님을 확인해야 합니다. 이는 작업을 수행하는 동안 예외가 발생했음을 의미합니다. 이 경우 다음을 수행하려고 합니다.
- 레이블에 사용자 친화적인 메시지
ExceptionDetails
표시 - 예외가 처리되었음을 나타냅니다.
- GridView 행을 편집 모드로 유지
다음 코드는 이러한 목표를 달성합니다.
protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
if (e.Exception != null)
{
// Display a user-friendly message
ExceptionDetails.Visible = true;
ExceptionDetails.Text = "There was a problem updating the product. ";
if (e.Exception.InnerException != null)
{
Exception inner = e.Exception.InnerException;
if (inner is System.Data.Common.DbException)
ExceptionDetails.Text +=
"Our database is currently experiencing problems." +
"Please try again later.";
else if (inner is NoNullAllowedException)
ExceptionDetails.Text +=
"There are one or more required fields that are missing.";
else if (inner is ArgumentException)
{
string paramName = ((ArgumentException)inner).ParamName;
ExceptionDetails.Text +=
string.Concat("The ", paramName, " value is illegal.");
}
else if (inner is ApplicationException)
ExceptionDetails.Text += inner.Message;
}
// Indicate that the exception has been handled
e.ExceptionHandled = true;
// Keep the row in edit mode
e.KeepInEditMode = true;
}
}
이 이벤트 처리기는 e.Exception
이(가) null
인지 확인하는 것으로 시작합니다. 그렇지 않은 경우, ExceptionDetails
Label의 Visible
속성이 true
로 설정되고 Text
속성은 "제품 업데이트에 문제가 발생했습니다."로 설정됩니다. throw된 실제 예외의 세부 정보는 e.Exception
객체의 InnerException
속성에 있습니다. 이 내부 예외가 검사되고 특정 형식인 경우 레이블의 ExceptionDetails
속성에 Text
유용한 추가 메시지가 추가됩니다. 마지막으로 ExceptionHandled
와 KeepInEditMode
속성은 모두 true
로 설정됩니다.
그림 9는 제품 이름을 생략할 때 이 페이지의 스크린샷을 보여줍니다. 그림 10은 잘못된 UnitPrice
값(-50)을 입력할 때의 결과를 보여 줍니다.
그림 9: BoundField에 ProductName
값이 포함되어야 합니다(전체 크기 이미지를 보려면 클릭).
그림 10: 음수 UnitPrice
값이 허용되지 않음(전체 크기 이미지를 보려면 클릭)
e.ExceptionHandled
속성을 true
로 설정하여 RowUpdated
이벤트 처리기가 예외를 처리했음을 나타냅니다. 따라서 예외는 ASP.NET 런타임까지 전파되지 않습니다.
비고
그림 9 및 10은 잘못된 사용자 입력으로 인해 발생한 예외를 처리하는 정상적인 방법을 보여 줍니다. 그러나 ASP.NET 페이지에서 클래스 ProductsBLL
의 메서드를 호출 UpdateProduct
하기 전에 사용자의 입력이 유효한지 확인해야 하므로 이러한 잘못된 입력은 처음에 비즈니스 논리 계층에 도달하지 않는 것이 이상적입니다. 다음 자습서에서는 편집 및 삽입 인터페이스에 유효성 검사 컨트롤을 추가하여 비즈니스 논리 계층에 제출된 데이터가 비즈니스 규칙을 준수하는지 확인하는 방법을 알아보세요. 유효성 검사 컨트롤은 사용자가 제공한 데이터가 유효할 때까지 메서드 호출을 방지 UpdateProduct
할 뿐만 아니라 데이터 입력 문제를 식별하기 위한 보다 유익한 사용자 환경을 제공합니다.
3단계: BLL-Level 예외를 정상적으로 처리
데이터를 삽입, 업데이트 또는 삭제할 때 데이터 액세스 계층은 데이터 관련 오류에 직면하여 예외를 throw할 수 있습니다. 데이터베이스가 오프라인 상태이거나, 필요한 데이터베이스 테이블 열에 지정된 값이 없거나, 테이블 수준 제약 조건이 위반되었을 수 있습니다. 엄격하게 데이터 관련 예외 외에도 비즈니스 논리 계층은 예외를 사용하여 비즈니스 규칙이 위반된 시기를 나타낼 수 있습니다. 예를 들어 비즈니스 논리 계층 만들기 자습서에서는 원래 UpdateProduct
오버로드에 비즈니스 규칙 검사를 추가했습니다. 특히 사용자가 제품을 중단된 것으로 표시하는 경우 공급업체에서 제공하는 유일한 제품이 아님을 요구했습니다. 이 조건을 위반하면 ApplicationException
예외가 발생했습니다.
UpdateProduct
이 자습서에서 만든 오버로드의 경우 필드를 원래 UnitPrice
값의 두 배 이상인 새 값으로 설정하지 못하도록 하는 UnitPrice
비즈니스 규칙을 추가해 보겠습니다. 이를 위해, 오버로드 UpdateProduct
를 조정하여 이 검사를 수행하고, 규칙을 위반하는 경우 예외 ApplicationException
를 던지도록 합니다. 업데이트된 메서드는 다음과 같습니다.
public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock,
int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
// Make sure the price has not more than doubled
if (unitPrice != null && !product.IsUnitPriceNull())
if (unitPrice > product.UnitPrice * 2)
throw new ApplicationException(
"When updating a product price," +
" the new price cannot exceed twice the original price.");
product.ProductName = productName;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
이 변경으로 인해 기존 가격의 두 배 이상인 가격 업데이트가 발생하면 ApplicationException
가 발생하게 됩니다. DAL에서 발생한 예외처럼, BLL에서 발생한 ApplicationException
는 GridView의 RowUpdated
이벤트 처리기에서 감지되고 처리될 수 있습니다. 실제로 기록된 RowUpdated
이벤트 처리기의 코드는 이 예외를 올바르게 감지하고 ApplicationException
의 Message
속성 값을 표시합니다. 그림 11은 사용자가 Chai의 가격을 $50.00로 업데이트하려고 할 때 스크린샷을 보여 줍니다. 이는 현재 가격인 $19.95의 두 배 이상입니다.
그림 11: 비즈니스 규칙은 제품 가격의 두 배 이상인 가격 인상을 허용하지 않습니다(전체 크기 이미지를 보려면 클릭).
비고
이상적으로는 비즈니스 논리 규칙이 메서드 오버로드에서 벗어나 공통 메서드로 리팩터링하는 것이 좋습니다. 이는 독자를 위한 연습으로 남아 있습니다.
요약
삽입, 업데이트, 삭제 작업 중, 데이터 웹 컨트롤과 ObjectDataSource 모두 실제 작업을 둘러싸는 사전 및 사후 수준 이벤트를 발생시킵니다. 이 자습서와 이전 자습서에서 보았듯이 편집 가능한 GridView로 작업할 때 GridView의 RowUpdating
이벤트가 발생한 다음 ObjectDataSource의 Updating
이벤트가 발생합니다. 이때 업데이트 명령은 ObjectDataSource의 기본 개체에 적용됩니다. 작업이 완료되면 ObjectDataSource의 Updated
이벤트가 발생한 다음 GridView의 RowUpdated
이벤트가 발생합니다.
작업 결과를 검사하고 응답하기 위해 입력 매개 변수 또는 사후 수준 이벤트를 사용자 지정하기 위해 사전 수준 이벤트에 대한 이벤트 처리기를 만들 수 있습니다. 사후 수준 이벤트 처리기는 작업 중에 예외가 발생했는지 여부를 감지하는 데 가장 일반적으로 사용됩니다. 예외가 발생할 경우 이러한 사후 수준 이벤트 처리기는 필요에 따라 예외를 자체적으로 처리할 수 있습니다. 이 자습서에서는 친숙한 오류 메시지를 표시하여 이러한 예외를 처리하는 방법을 알아보았습니다.
다음 자습서에서는 데이터 서식 문제(예: 부정 UnitPrice
입력)로 인해 발생하는 예외의 가능성을 줄이는 방법을 살펴보겠습니다. 특히 편집 및 삽입 인터페이스에 유효성 검사 컨트롤을 추가하는 방법을 살펴보겠습니다.
행복한 프로그래밍!
작성자 정보
7개의 ASP/ASP.NET 책의 저자이자 4GuysFromRolla.com 창립자인 Scott Mitchell은 1998년부터 Microsoft 웹 기술을 연구해 왔습니다. Scott은 독립 컨설턴트, 트레이너 및 작가로 일합니다. 그의 최신 책은 Sams Teach Yourself ASP.NET 2.0 in 24 Hours입니다. 그에게 mitchell@4GuysFromRolla.com으로 연락할 수 있습니다.
특별히 감사드립니다.
이 자습서 시리즈는 많은 유용한 검토자가 검토했습니다. 이 자습서의 수석 검토자는 Liz Shulok였습니다. 예정된 MSDN 문서를 검토하는 데 관심이 있으신가요? 그렇다면 mitchell@4GuysFromRolla.com으로 메시지를 보내 주세요.